SlideShare a Scribd company logo
Kotlin
Mullets
Business on the Front End, Party in Back
Chet Haase, Developer Advocate, Android
James Ward, Developer Advocate, Google Cloud
Demo!
Track
● Track
Motion
Events
Record
● Add
Locations
to Path(s)
View
● Invalidate
ML
● Ready to
Analyze
Draw
● Draw Path
Data
User Drawings
UI Layout
<androidx.constraintlayout.widget.ConstraintLayout ...>
<Spinner ... />
<Button ... />
<Button ... />
<ToggleButton... />
<Button ... />
<TextView... />
<TextView ... />
<TextView ... />
<TextView ... />
<com.jamesward.airdraw.DrawingCanvas ... />
</androidx.constraintlayout.widget.ConstraintLayout>
UI Layout, with custom view
<androidx.constraintlayout.widget.ConstraintLayout ...>
< ... />
<com.jamesward.airdraw.DrawingCanvas
android:id="@+id/drawingCanvas"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
android:background="@android:color/black"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="@id/rightGuide"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@id/leftGuide"
app:layout_constraintTop_toBottomOf="@+id/fourthGuess" />
</androidx.constraintlayout.widget.ConstraintLayout>
DrawingCanvas
class DrawingCanvas(context: Context?, attr: AttributeSet) :
View(context, attr) {
val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val drawingPath = Path()
fun getBitmap():Bitmap { ... }
fun setBitmap(bitmap: Bitmap) { ... }
override fun onTouchEvent(event: MotionEvent?): Boolean { ... }
fun clear() { ... }
override fun onDraw(canvas: Canvas?) { ... }
}
Track Motion Events
override fun onTouchEvent(event: MotionEvent?): Boolean {
var handled = false
if (event != null) {
when {
event.action == MotionEvent.ACTION_DOWN -> {
drawingPath.moveTo(event.x, event.y)
drawingPath.lineTo(event.x, event.y)
invalidate()
handled = true
}
event.action == MotionEvent.ACTION_MOVE -> {
drawingPath.lineTo(event.x, event.y)
invalidate()
handled = true
}
event.action == MotionEvent.ACTION_UP -> {
handled = true
}
}
}
return handled
}
Draw Path
override fun onDraw(canvas: Canvas?) {
canvas?.drawPath(drawingPath, paint)
}
Interlude: Learning About Machine Learning
Tensor FlowTraining Data Model
TFLite Model
On-Device
Model
User Data
Results
Interlude: ML
Results
Labels
Barcodes
Text
Objects
Faces
Translations
+ Confidence Values
Or…. Use Built-in Models
Tensor FlowTraining Data Model
TFLite Model
On-Device
Model
User Data
Results
Convert Drawing
● Extract
Bitmap
Setup Labeler
● Create
Firebase
Vision
Image
● Create On-
Device
Labeler
Send Data
● Process
Data
Receive
Results
● onSuccess
callback
receives
results
Get Image Labels
Display
Results
● Show
Labels and
Confidence
Get Bitmap Data
fun getBitmap():Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
draw(canvas)
return bitmap
}
KTX
Get Bitmap Data
fun getBitmap():Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
draw(canvas)
return bitmap
}
Get Bitmap Data
fun getBitmap() = createBitmap(width, height).applyCanvas(::draw)
fun getBitmap():Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
draw(canvas)
return bitmap
}
Call Image Labeler
val firebaseImage = FirebaseVisionImage.fromBitmap(bitmap)
val labeler = FirebaseVision.getInstance().onDeviceImageLabeler
labeler.processImage(firebaseImage).addOnSuccessListener {
// . . .
}
{
{“label”, confidence},
{“label”, confidence},
…
}
Digital Processing: MNIST
Image: ©Josef Steppan, https://commons.wikimedia.org/wiki/File:MnistExamples.png
Modified National Institute of Standards and Technology
Hand-written digits for image-processing
Create Interpreter
● Load model
● Create
Interpreter
with model
Get Bitmap
● Get user
data as
bitmap
● Resize to
match
model
Run Interpreter
● Process
data
Process
Results
● Sort results
by
confidence
Get Image Labels
Display
Results
● Show
results and
confidence
Get Model as ByteBuffer
val model = loadModelFile()
private fun loadModelFile(): ByteBuffer {
val fileDescriptor = assets.openFd("mnist.tflite")
val inputStream = FileInputStream(fileDescriptor.fileDescriptor)
val fileChannel = inputStream.channel
val startOffset = fileDescriptor.startOffset
val declaredLength = fileDescriptor.declaredLength
return fileChannel.map(FileChannel.MapMode.READ_ONLY,
startOffset, declaredLength)
}
Initialize Interpreter, Sizes
val options = Interpreter.Options()
options.setUseNNAPI(true)
val interpreter = Interpreter(model, options)
val inputShape = interpreter.getInputTensor(0).shape()
inputImageWidth = inputShape[1]
inputImageHeight = inputShape[2]
modelInputSize = 4 * inputImageWidth * inputImageHeight
Get Results
private fun detectDigit(bitmap: Bitmap,
resultsListener: (List<LabelAnnotation>) -> Unit) {
lateinit var digitSequence: List<Pair<String,Float>>
val resizedImage = Bitmap.createScaledBitmap(bitmap,
inputImageWidth, inputImageHeight, true)
val byteBuffer = convertBitmapToByteBuffer(resizedImage)
val result = Array(1) { FloatArray(10) }
interpreter.run(byteBuffer, result)
// format and display results
}
Demo!
KTS
Type-safe Builds
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation("androidx.appcompat:appcompat:1.1.0")
...
}
build.gradle.kts
fun Project.dependencies(configuration: DependencyHandlerScope.() -> Unit) =
DependencyHandlerScope.of(dependencies).configuration()
fun DependencyHandler.`implementation`(dependencyNotation: Any): Dependency? =
add("implementation", dependencyNotation)
Declarative(ish) & Imperative
if (startParameter.taskRequests.find { it.args.contains(":server:shadowJar") } == null) {
include("common", "android", "web", "server")
} else {
include("common", "web", "server")
}
settings.gradle.kts
ML in the Cloud
val firebaseImage = FirebaseVisionImage.fromBitmap(bitmap)
val labeler = FirebaseVision.getInstance().onDeviceImageLabeler
labeler.processImage(firebaseImage).addOnSuccessListener { … }
On-Device Labeling
ML in the Cloud
val firebaseImage = FirebaseVisionImage.fromBitmap(bitmap)
val labeler = FirebaseVision.getInstance().cloudImageLabeler
labeler.processImage(firebaseImage).addOnSuccessListener { … }
Cloud Labeling
Demo!
Coroutines
Get Results
private fun detectDigit(bitmap: Bitmap,
resultsListener: (List<LabelAnnotation>) -> Unit) {
lateinit var digitSequence: List<Pair<String,Float>>
val resizedImage = Bitmap.createScaledBitmap(bitmap,
inputImageWidth, inputImageHeight, true)
val byteBuffer = convertBitmapToByteBuffer(resizedImage)
val result = Array(1) { FloatArray(10) }
interpreter.run(byteBuffer, result)
// format and display results
}
Get Results
private fun detectDigit(bitmap: Bitmap,
resultsListener: (List<LabelAnnotation>) -> Unit) {
lateinit var digitSequence: List<Pair<String,Float>>
val resizedImage = Bitmap.createScaledBitmap(bitmap,
inputImageWidth, inputImageHeight, true)
val byteBuffer = convertBitmapToByteBuffer(resizedImage)
val result = Array(1) { FloatArray(10) }
interpreter.run(byteBuffer, result)
// format and display results
}
Get Results
private suspend fun detectDigit(bitmap: Bitmap,
resultsListener: (List<LabelAnnotation>) -> Unit) {
lateinit var digitSequence: List<Pair<String,Float>>
val resizedImage = Bitmap.createScaledBitmap(bitmap,
inputImageWidth, inputImageHeight, true)
val byteBuffer = convertBitmapToByteBuffer(resizedImage)
val result = Array(1) { FloatArray(10) }
withContext(Dispatchers.IO) {
interpreter.run(byteBuffer, result)
}
// format and display results
}
Server-Side
Draw
● Capture
Bitmap
Phone ML
● Predict with
Local Model
● Display
Results
REST Service
● Send
Bitmap &
Results to
REST API
Browser
● Poll d' Bus
● Display
Bitmap &
Results
Pub/Sub
● On d' Bus
Browser
Bus
Server
Shared Common
Root Gradle Project
Common
ServerAndroid
data class LabelAnnotation(
val description: String,
val score: Float)
data class ImageResult(
val image: ByteArray,
val labelAnnotations: List<LabelAnnotation>)
common/src/commonMain/kotlin/Data.kt
Multiplatform Common
plugins {
id("com.android.library")
kotlin("multiplatform")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
}
android {
defaultConfig {
compileSdkVersion(29)
}
}
common/build.gradle.ktx
Multiplatform Common
kotlin {
sourceSets {
commonMain {
dependencies {
implementation(kotlin("stdlib"))
}
}
}
jvm {
val main by compilations.getting {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
}
}
android()
}
common/build.gradle.ktx
Multiplatform Common Dependency
dependencies {
implementation(project(":common"))
}
android/build.gradle.ktx
server/build.gradle.kts
Cloud Service - Deps
plugins {
kotlin("kapt")
}
dependencies {
implementation("io.micronaut:micronaut-runtime:1.2.6")
implementation("io.micronaut:micronaut-http-server-netty:1.2.6")
implementation("io.micronaut:micronaut-views:1.2.0")
api("ch.qos.logback:logback-classic:1.2.3")
api("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.1")
api("org.thymeleaf:thymeleaf:3.0.11.RELEASE")
kapt("io.micronaut:micronaut-inject-java:1.2.6")
}
server/build.gradle.ktx
Cloud Service
@Post("/show")
fun show(@Body imageResult: ImageResult): HttpResponse<String> {
bus.put(imageResult)
return HttpResponse.ok("")
}
@Get("/events")
fun events(): HttpResponse<ImageResult> {
val maybe = bus.take()
return if (maybe != null)
HttpResponse.ok(maybe)
else
HttpResponse.noContent()
}
server/src/main/kotlin/WebApp.kt
Client
@Client("${drawurl}")
interface DrawService {
@Post("/show")
fun show(@Body imageResult: ImageResult): Single<Unit>
}
@Inject
var drawService: DrawService? = null
drawService?.show(imageResult)?.subscribe()?.dispose()
android/src/main/kotlin/MainActivity.kt
Setup DI
class BaseApplication: Application() {
private var ctx: ApplicationContext? = null
override fun onCreate() {
super.onCreate()
val pm = applicationContext.packageManager
val ai = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
val propertySource = AndroidMetadataPropertySource(ai.metaData)
val appCtxBuilder = applicationContext.build(MainActivity::class.java, Environment.ANDROID)
ctx = appCtxBuilder.propertySources(propertySource).start()
registerActivityLifecycleCallbacks(object: ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity?, bundle: Bundle?) {
if (activity != null) ctx?.inject(activity)
}
})
}
android/src/main/kotlin/BaseApplication.kt
Config via AndroidManfest.xml
<meta-data android:name="drawurl" android:value="${drawurl}"/>
val drawUrl: String? by project
if (drawUrl != null) {
manifestPlaceholders = mapOf("drawurl" to drawUrl)
}
else {
// 10.0.2.2 is the IP for your machine from the Android emulator
manifestPlaceholders = mapOf("drawurl" to "http://10.0.2.2:8080")
}
android/build.gradle.ktx
android/src/main/AndroidManifest.xml
drawUrl=http://192.168.1.23:8080
gradle.properties
Cloud Run, Run
FROM adoptopenjdk/openjdk8 as builder
WORKDIR /app
COPY . /app
RUN ./gradlew --no-daemon --console=plain :server:shadowJar
FROM adoptopenjdk/openjdk8:jre
COPY --from=builder /app/server/build/libs/server.jar /server.jar
RUN apt-get update && apt-get install -y --no-install-recommends fontconfig
CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/server.jar"]
Dockerfile
Build & Deploy
steps:
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA', '/workspace']
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA']
- name: 'gcr.io/cloud-builders/gcloud'
args: ['beta', 'run', 'deploy', '--image=gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA',
'--platform=managed', '--project=$PROJECT_ID', '--region=us-central1', '--allow-
unauthenticated', '--memory=512Mi', '$REPO_NAME']
cloudbuild.yaml
Demo!
Jetpack Compose
Jetpack Compose
Reactive, declarative UI Toolkit
Kotlin-first
Developer Preview
(Pre-Alpha)
Demo!
Track
● Track
Motion
Events
Record
● Add
Locations
to Path(s)
View
● Invalidate
ML
● Ready to
Analyze
Draw
● Draw Path
Data
User Drawings
In Compose!
UI Layout
@Composable
fun BuildUI(guesses: Guesses = Guesses()) {
MaterialTheme() {
val invalidator = +invalidate
Column(Spacing(8.dp), crossAxisAlignment = CrossAxisAlignment.Stretch) {
FlexRow(mainAxisAlignment = MainAxisAlignment.Center) {
val radioOptions = listOf("Shape", "Digit")
val (selectedOption, onOptionSelected) = +state { radioOptions[0] }
inflexible {
RadioGroup(
options = radioOptions,
selectedOption = selectedOption,
onSelectedChange = onOptionSelected
)
}
flexible(1f) {
Column(crossAxisAlignment = CrossAxisAlignment.Stretch) {
Button(text = "Local", ...)
Button(text = "Cloud", ...)
}
}
}
Button(text = "Sensorify", ...)
HeightSpacer(8.dp)
GuessDisplay(guesses)
HeightSpacer(8.dp)
DrawingCanvas(path)
HeightSpacer(8.dp)
Button(text = "Clear", ...)
HeightSpacer(8.dp)
}
}
}
UI Layout
@Composable
fun BuildUI(guesses: Guesses = Guesses()) {
MaterialTheme() {
val invalidator = +invalidate
Column(Spacing(8.dp), crossAxisAlignment = CrossAxisAlignment.Stretch) {
FlexRow(mainAxisAlignment = MainAxisAlignment.Center) {
val radioOptions = listOf("Shape", "Digit")
val (selectedOption, onOptionSelected) = +state { radioOptions[0] }
inflexible {
RadioGroup(
options = radioOptions,
selectedOption = selectedOption,
onSelectedChange = onOptionSelected
)
}
flexible(1f) {
Column(crossAxisAlignment = CrossAxisAlignment.Stretch) {
Button(text = "Local", ...)
Button(text = "Cloud", ...)
}
}
}
Button(text = "Sensorify", ...)
HeightSpacer(8.dp)
GuessDisplay(guesses)
HeightSpacer(8.dp)
DrawingCanvas(path)
HeightSpacer(8.dp)
Button(text = "Clear", ...)
HeightSpacer(8.dp)
}
}
}
DrawingCanvas
@Composable
fun DrawingCanvas(path: Path) {
val invalidate = +invalidate
RawDragGestureDetector(dragObserver = MyDragObserver(path, invalidate)) {
Surface(color = Color.Black) {
Container(modifier = ExpandedHeight, width = 200.dp, height = 350.dp) {
Draw { canvas, parentSize ->
canvas.drawPath(path, fingerPaint)
}
}
}
}
}
DrawingCanvas
@Composable
fun DrawingCanvas(path: Path) {
val invalidate = +invalidate
RawDragGestureDetector(dragObserver = MyDragObserver(path, invalidate)) {
Surface(color = Color.Black) {
Container(modifier = ExpandedHeight, width = 200.dp, height = 350.dp) {
Draw { canvas, parentSize ->
canvas.drawPath(path, fingerPaint)
}
}
}
}
}
Track Motion Events
class MyDragObserver(val dragPath: Path, val recompose: () -> Unit): DragObserver {
override fun onStart(downPosition: PxPosition) {
dragPath.moveTo(downPosition.x.value, downPosition.y.value)
}
override fun onDrag(dragDistance: PxPosition): PxPosition {
dragPath.relativeLineTo(dragDistance.x.value, dragDistance.y.value)
recompose()
return dragDistance
}
}
Machine Learning Code
Button(text = "Local", onClick = {
machineLearningStuff.detectObject(true, bitmap!!,
selectedOption == "Shape") {
// display results in Text objects...
}
})
Web UI
Shared Common
Root Gradle Project
Common
ServerAndroid Web
Jar
Kotlin JS - Build
plugins {
kotlin("js")
}
dependencies {
implementation(kotlin("stdlib-js"))
implementation(project(":common"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.2")
implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.6.12")
}
web/build.gradle.kts
Kotlin JS - Assemble JsJar
tasks.withType<org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile> {
kotlinOptions {
metaInfo = false
sourceMap = true
outputFile = "$buildDir/classes/kotlin/main/${project.name}.js"
}
}
task<Copy>("assembleJsLib") {
from(...)
into("$buildDir/classes/kotlin/main")
dependsOn("mainClasses")
}
tasks {
JsJar {
into("META-INF/resources")
dependsOn("assembleJsLib")
}
}
web/build.gradle.kts
Kotlin JS - Polling
fun main() {
GlobalScope.launch { poll() }
}
suspend fun poll() {
val res = window.fetch("/events").await()
when (res.status.toInt()) {
200 -> {
val json = res.json().await().asDynamic()
...
val imageResult = ImageResult(byteArray, labelAnnotations)
...
}
}
window.setTimeout({ GlobalScope.launch { poll() } }, 1000)
}
web/src/main/kotlin/Main.kt
Kotlin JS - Rendering
val urlImage = "url('data:image/png;base64,${imageResult.image.decodeToString()}')"
document.body?.style?.backgroundImage = urlImage
document.body?.clear()
val div = document.create.div()
imageResult.labelAnnotations.forEach { labelAnnotation ->
div.append {
p {
+"${labelAnnotation.description} = ${round(labelAnnotation.score *
100)}%"
}
}
}
document.body?.append(div)
web/src/main/kotlin/Main.kt
Kotlin JS - Serving Static Resources
micronaut.router.static-resources.resources.enabled=true
micronaut.router.static-resources.resources.paths=classpath:META-INF/resources
micronaut.router.static-resources.resources.mapping=/resources/**
server/src/main/resources/application.properties
dependencies {
api(files("../web/build/libs/web.jar"))
}
tasks.withType<org.jetbrains.kotlin.gradle.internal.KaptWithKotlincTask> {
dependsOn(":web:JsJar")
}
server/build.gradle.kts
Kotlin JS - HTML Page
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Air Draw</title>
<link rel="stylesheet" href="assets/index.css">
<script src="resources/kotlin.js"></script>
<script src="resources/kotlinx-coroutines-core.js"></script>
<script src="resources/kotlinx-html-js.js"></script>
<script src="resources/kotlinx-serialization-kotlinx-serialization-
runtime.js"></script>
<script src="resources/common.js"></script>
<script src="resources/web.js"></script>
</head>
<body>
waiting for drawings...
</body>
</html>
server/src/main/kotlin/resources/views/index.html
Multi-Module Project
One Project To Kotlin Them All
Root Gradle Project
Common
ServerAndroid Web
build.gradle.kts
common/build.gradle.kts
android/build.gradle.kts
server/build.gradle.kts
web/build.gradle.kts
One Project To Rule Them All
buildscript {
repositories {
mavenLocal()
mavenCentral()
jcenter()
google()
}
dependencies {
classpath(kotlin("gradle-plugin", "1.3.61"))
classpath("com.android.tools.build:gradle:4.0.0-alpha04")
}
}
build.gradle.kts (Root Gradle Project)
Move Data Processing to the Server
Demo!
Air Draw
● Capture
Sensor Data
REST Service
● Smoooooth
Data
● Render to
Image
Cloud ML
● Send
Bitmap to
Cloud ML
● Send
Bitmap &
results to
Phone
Browser
● Poll d' Bus
● Display
Bitmap &
Results
Pub/Sub
● On d' Bus
Mapping Sensor Data to an Image
Sensors & Vision
data class Orientation(val azimuth: Float, val pitch: Float, val timestamp: Long)
SensorManager.getRotationMatrixFromVector(rotationMatrix, e.values)
val m = FloatArray(3)
SensorManager.getOrientation(rotationMatrix, orientationAngles)
val orientation = Orientation(m[0], m[1], e.timestamp)
readings.add(orientation)
common/src/commonMain/kotlin/Data.kt
SEND d' DATA!
@Client("${drawurl}")
interface DrawService {
@Post("/draw")
fun draw(@Body readings: List<Orientation>): Single<ImageResult>
}
machineLearningStuff.sensorAction(on) { orientations ->
val imageResultMaybe = drawService?.draw(orientations)?.blockingGet()
imageResultMaybe?.let { imageResult ->
displayResults(imageResult)
}
}
android/src/main/kotlin/MainActivity.kt
RECEIVE d’ DATA!
@Post("/draw")
fun draw(@Body readingsSingle: List<Orientation>):
Single<ImageResult> {
return readingsSingle.map { readings ->
airDraw.run(readings)?.let { imageResult ->
bus.put(imageResult)
HttpResponse.ok(imageResult)
}
}
}
server/src/main/kotlin/WebApp.kt
Things get crazy
val xl = KrigingInterpolation1D(t, x)
...
import java.awt.image.BufferedImage
val bi = BufferedImage(canvas.width, canvas.height, BufferedImage.TYPE_INT_ARGB)
...
val imgBytes = ByteString.copyFrom(bytes)
val img = Image.newBuilder().setContent(imgBytes).build()
val feature = Feature.newBuilder().setType(Type.LABEL_DETECTION).build()
val request = AnnotateImageRequest.newBuilder().addFeatures(feature).setImage(img).build()
val r = myImageAnnotatorClient.imageAnnotatorClient.batchAnnotateImages(arrayListOf(request))
server/src/main/kotlin/WebApp.kt
Pitfalls
● Gradle Build Plugins
● JSON
● Externalizing the URL
● Math is hard
● ML is hard
● Compose is Young
github.com/GoogleCloudPlatform/air-draw-demo

More Related Content

What's hot

Drawing with the HTML5 Canvas
Drawing with the HTML5 CanvasDrawing with the HTML5 Canvas
Drawing with the HTML5 Canvas
Henry Osborne
 
Real life XNA
Real life XNAReal life XNA
Real life XNA
Johan Lindfors
 
HTML 5_Canvas
HTML 5_CanvasHTML 5_Canvas
HTML 5_Canvas
Vishakha Vaidya
 
Intro to HTML5
Intro to HTML5Intro to HTML5
Intro to HTML5
Jussi Pohjolainen
 
HTML5って必要?
HTML5って必要?HTML5って必要?
HTML5って必要?
GCS2013
 
A More Flash Like Web?
A More Flash Like Web?A More Flash Like Web?
A More Flash Like Web?
Murat Can ALPAY
 
Reactive data visualisations with Om
Reactive data visualisations with OmReactive data visualisations with Om
Reactive data visualisations with Om
Anna Pawlicka
 
The Ring programming language version 1.5.3 book - Part 70 of 184
The Ring programming language version 1.5.3 book - Part 70 of 184The Ring programming language version 1.5.3 book - Part 70 of 184
The Ring programming language version 1.5.3 book - Part 70 of 184
Mahmoud Samir Fayed
 
SVCC 2013 D3.js Presentation (10/05/2013)
SVCC 2013 D3.js Presentation (10/05/2013)SVCC 2013 D3.js Presentation (10/05/2013)
SVCC 2013 D3.js Presentation (10/05/2013)
Oswald Campesato
 
Ui perfomance
Ui perfomanceUi perfomance
Ui perfomance
Cleveroad
 
HTML5 - Daha Flash bir web?
HTML5 - Daha Flash bir web?HTML5 - Daha Flash bir web?
HTML5 - Daha Flash bir web?
Ankara JUG
 
DroidKnight 2018 State machine by Selaed class
DroidKnight 2018 State machine by Selaed classDroidKnight 2018 State machine by Selaed class
DroidKnight 2018 State machine by Selaed class
Myeongin Woo
 
The Ring programming language version 1.8 book - Part 65 of 202
The Ring programming language version 1.8 book - Part 65 of 202The Ring programming language version 1.8 book - Part 65 of 202
The Ring programming language version 1.8 book - Part 65 of 202
Mahmoud Samir Fayed
 
Canvas - HTML 5
Canvas - HTML 5Canvas - HTML 5
Canvas - HTML 5
Jaeni Sahuri
 
Exploring Canvas
Exploring CanvasExploring Canvas
Exploring Canvas
Kevin Hoyt
 
How to make a video game
How to make a video gameHow to make a video game
How to make a video game
dandylion13
 
Graphics & Animation with HTML5
Graphics & Animation with HTML5Graphics & Animation with HTML5
Graphics & Animation with HTML5
Knoldus Inc.
 
Sokoban Game Development Using Java ( Updated using Screenshots & Class Diagr...
Sokoban Game Development Using Java ( Updated using Screenshots & Class Diagr...Sokoban Game Development Using Java ( Updated using Screenshots & Class Diagr...
Sokoban Game Development Using Java ( Updated using Screenshots & Class Diagr...
British Council
 
HTML5 Canvas - Let's Draw!
HTML5 Canvas - Let's Draw!HTML5 Canvas - Let's Draw!
HTML5 Canvas - Let's Draw!
Phil Reither
 
Css5 canvas
Css5 canvasCss5 canvas
Css5 canvas
Vadim Spiridenko
 

What's hot (20)

Drawing with the HTML5 Canvas
Drawing with the HTML5 CanvasDrawing with the HTML5 Canvas
Drawing with the HTML5 Canvas
 
Real life XNA
Real life XNAReal life XNA
Real life XNA
 
HTML 5_Canvas
HTML 5_CanvasHTML 5_Canvas
HTML 5_Canvas
 
Intro to HTML5
Intro to HTML5Intro to HTML5
Intro to HTML5
 
HTML5って必要?
HTML5って必要?HTML5って必要?
HTML5って必要?
 
A More Flash Like Web?
A More Flash Like Web?A More Flash Like Web?
A More Flash Like Web?
 
Reactive data visualisations with Om
Reactive data visualisations with OmReactive data visualisations with Om
Reactive data visualisations with Om
 
The Ring programming language version 1.5.3 book - Part 70 of 184
The Ring programming language version 1.5.3 book - Part 70 of 184The Ring programming language version 1.5.3 book - Part 70 of 184
The Ring programming language version 1.5.3 book - Part 70 of 184
 
SVCC 2013 D3.js Presentation (10/05/2013)
SVCC 2013 D3.js Presentation (10/05/2013)SVCC 2013 D3.js Presentation (10/05/2013)
SVCC 2013 D3.js Presentation (10/05/2013)
 
Ui perfomance
Ui perfomanceUi perfomance
Ui perfomance
 
HTML5 - Daha Flash bir web?
HTML5 - Daha Flash bir web?HTML5 - Daha Flash bir web?
HTML5 - Daha Flash bir web?
 
DroidKnight 2018 State machine by Selaed class
DroidKnight 2018 State machine by Selaed classDroidKnight 2018 State machine by Selaed class
DroidKnight 2018 State machine by Selaed class
 
The Ring programming language version 1.8 book - Part 65 of 202
The Ring programming language version 1.8 book - Part 65 of 202The Ring programming language version 1.8 book - Part 65 of 202
The Ring programming language version 1.8 book - Part 65 of 202
 
Canvas - HTML 5
Canvas - HTML 5Canvas - HTML 5
Canvas - HTML 5
 
Exploring Canvas
Exploring CanvasExploring Canvas
Exploring Canvas
 
How to make a video game
How to make a video gameHow to make a video game
How to make a video game
 
Graphics & Animation with HTML5
Graphics & Animation with HTML5Graphics & Animation with HTML5
Graphics & Animation with HTML5
 
Sokoban Game Development Using Java ( Updated using Screenshots & Class Diagr...
Sokoban Game Development Using Java ( Updated using Screenshots & Class Diagr...Sokoban Game Development Using Java ( Updated using Screenshots & Class Diagr...
Sokoban Game Development Using Java ( Updated using Screenshots & Class Diagr...
 
HTML5 Canvas - Let's Draw!
HTML5 Canvas - Let's Draw!HTML5 Canvas - Let's Draw!
HTML5 Canvas - Let's Draw!
 
Css5 canvas
Css5 canvasCss5 canvas
Css5 canvas
 

Similar to Kotlin Mullets

Big Data for each one of us
Big Data for each one of usBig Data for each one of us
Big Data for each one of us
OSCON Byrum
 
bbyopenApp_Code.DS_StorebbyopenApp_CodeVBCodeGoogleMaps.docx
bbyopenApp_Code.DS_StorebbyopenApp_CodeVBCodeGoogleMaps.docxbbyopenApp_Code.DS_StorebbyopenApp_CodeVBCodeGoogleMaps.docx
bbyopenApp_Code.DS_StorebbyopenApp_CodeVBCodeGoogleMaps.docx
ikirkton
 
Writing videogames with titanium appcelerator
Writing videogames with titanium appceleratorWriting videogames with titanium appcelerator
Writing videogames with titanium appcelerator
Alessio Ricco
 
MOPCON 2014 - Best software architecture in app development
MOPCON 2014 - Best software architecture in app developmentMOPCON 2014 - Best software architecture in app development
MOPCON 2014 - Best software architecture in app development
anistar sung
 
mobl
moblmobl
mobl
zefhemel
 
What's New in Android
What's New in AndroidWhat's New in Android
What's New in Android
Robert Cooper
 
Google Fit, Android Wear & Xamarin
Google Fit, Android Wear & XamarinGoogle Fit, Android Wear & Xamarin
Google Fit, Android Wear & Xamarin
Peter Friese
 
mobl presentation @ IHomer
mobl presentation @ IHomermobl presentation @ IHomer
mobl presentation @ IHomer
zefhemel
 
Palestra - Utilizando tensorflow mobile e seus desafios
Palestra - Utilizando tensorflow mobile e seus desafiosPalestra - Utilizando tensorflow mobile e seus desafios
Palestra - Utilizando tensorflow mobile e seus desafios
Gustavo Monteiro
 
Windows Phone Launchers and Choosers
Windows Phone Launchers and ChoosersWindows Phone Launchers and Choosers
[1D6]RE-view of Android L developer PRE-view
[1D6]RE-view of Android L developer PRE-view[1D6]RE-view of Android L developer PRE-view
[1D6]RE-view of Android L developer PRE-view
NAVER D2
 
Work With Images
Work With ImagesWork With Images
Work With Images
Sergey Tarasevich
 
Android 3
Android 3Android 3
Android 3
Robert Cooper
 
Svcc 2013-d3
Svcc 2013-d3Svcc 2013-d3
Svcc 2013-d3
Oswald Campesato
 
Graph computation
Graph computationGraph computation
Graph computation
Sigmoid
 
Asynchronous Programming at Netflix
Asynchronous Programming at NetflixAsynchronous Programming at Netflix
Asynchronous Programming at Netflix
C4Media
 
Knockoutjs UG meeting presentation
Knockoutjs UG meeting presentationKnockoutjs UG meeting presentation
Knockoutjs UG meeting presentation
Valdis Iljuconoks
 
Massimo Artizzu - The tricks of Houdini: a magic wand for the future of CSS -...
Massimo Artizzu - The tricks of Houdini: a magic wand for the future of CSS -...Massimo Artizzu - The tricks of Houdini: a magic wand for the future of CSS -...
Massimo Artizzu - The tricks of Houdini: a magic wand for the future of CSS -...
Codemotion
 
Model View Intent on Android
Model View Intent on AndroidModel View Intent on Android
Model View Intent on Android
Cody Engel
 
XebiCon'17 : Faites chauffer les neurones de votre Smartphone avec du Deep Le...
XebiCon'17 : Faites chauffer les neurones de votre Smartphone avec du Deep Le...XebiCon'17 : Faites chauffer les neurones de votre Smartphone avec du Deep Le...
XebiCon'17 : Faites chauffer les neurones de votre Smartphone avec du Deep Le...
Publicis Sapient Engineering
 

Similar to Kotlin Mullets (20)

Big Data for each one of us
Big Data for each one of usBig Data for each one of us
Big Data for each one of us
 
bbyopenApp_Code.DS_StorebbyopenApp_CodeVBCodeGoogleMaps.docx
bbyopenApp_Code.DS_StorebbyopenApp_CodeVBCodeGoogleMaps.docxbbyopenApp_Code.DS_StorebbyopenApp_CodeVBCodeGoogleMaps.docx
bbyopenApp_Code.DS_StorebbyopenApp_CodeVBCodeGoogleMaps.docx
 
Writing videogames with titanium appcelerator
Writing videogames with titanium appceleratorWriting videogames with titanium appcelerator
Writing videogames with titanium appcelerator
 
MOPCON 2014 - Best software architecture in app development
MOPCON 2014 - Best software architecture in app developmentMOPCON 2014 - Best software architecture in app development
MOPCON 2014 - Best software architecture in app development
 
mobl
moblmobl
mobl
 
What's New in Android
What's New in AndroidWhat's New in Android
What's New in Android
 
Google Fit, Android Wear & Xamarin
Google Fit, Android Wear & XamarinGoogle Fit, Android Wear & Xamarin
Google Fit, Android Wear & Xamarin
 
mobl presentation @ IHomer
mobl presentation @ IHomermobl presentation @ IHomer
mobl presentation @ IHomer
 
Palestra - Utilizando tensorflow mobile e seus desafios
Palestra - Utilizando tensorflow mobile e seus desafiosPalestra - Utilizando tensorflow mobile e seus desafios
Palestra - Utilizando tensorflow mobile e seus desafios
 
Windows Phone Launchers and Choosers
Windows Phone Launchers and ChoosersWindows Phone Launchers and Choosers
Windows Phone Launchers and Choosers
 
[1D6]RE-view of Android L developer PRE-view
[1D6]RE-view of Android L developer PRE-view[1D6]RE-view of Android L developer PRE-view
[1D6]RE-view of Android L developer PRE-view
 
Work With Images
Work With ImagesWork With Images
Work With Images
 
Android 3
Android 3Android 3
Android 3
 
Svcc 2013-d3
Svcc 2013-d3Svcc 2013-d3
Svcc 2013-d3
 
Graph computation
Graph computationGraph computation
Graph computation
 
Asynchronous Programming at Netflix
Asynchronous Programming at NetflixAsynchronous Programming at Netflix
Asynchronous Programming at Netflix
 
Knockoutjs UG meeting presentation
Knockoutjs UG meeting presentationKnockoutjs UG meeting presentation
Knockoutjs UG meeting presentation
 
Massimo Artizzu - The tricks of Houdini: a magic wand for the future of CSS -...
Massimo Artizzu - The tricks of Houdini: a magic wand for the future of CSS -...Massimo Artizzu - The tricks of Houdini: a magic wand for the future of CSS -...
Massimo Artizzu - The tricks of Houdini: a magic wand for the future of CSS -...
 
Model View Intent on Android
Model View Intent on AndroidModel View Intent on Android
Model View Intent on Android
 
XebiCon'17 : Faites chauffer les neurones de votre Smartphone avec du Deep Le...
XebiCon'17 : Faites chauffer les neurones de votre Smartphone avec du Deep Le...XebiCon'17 : Faites chauffer les neurones de votre Smartphone avec du Deep Le...
XebiCon'17 : Faites chauffer les neurones de votre Smartphone avec du Deep Le...
 

More from James Ward

Koober Machine Learning
Koober Machine LearningKoober Machine Learning
Koober Machine Learning
James Ward
 
Introduction to Machine Learning
Introduction to Machine LearningIntroduction to Machine Learning
Introduction to Machine Learning
James Ward
 
Salesforce Campus Tour - Developer Intro
Salesforce Campus Tour - Developer IntroSalesforce Campus Tour - Developer Intro
Salesforce Campus Tour - Developer Intro
James Ward
 
Salesforce Campus Tour - Developer Advanced
Salesforce Campus Tour - Developer AdvancedSalesforce Campus Tour - Developer Advanced
Salesforce Campus Tour - Developer Advanced
James Ward
 
Salesforce Campus Tour - Declarative
Salesforce Campus Tour - DeclarativeSalesforce Campus Tour - Declarative
Salesforce Campus Tour - Declarative
James Ward
 
Integrating Clouds & Humans with Wearable Apps
Integrating Clouds & Humans with Wearable AppsIntegrating Clouds & Humans with Wearable Apps
Integrating Clouds & Humans with Wearable Apps
James Ward
 
Building Reactive Apps
Building Reactive AppsBuilding Reactive Apps
Building Reactive Apps
James Ward
 
Planet of the AOPs
Planet of the AOPsPlanet of the AOPs
Planet of the AOPs
James Ward
 

More from James Ward (8)

Koober Machine Learning
Koober Machine LearningKoober Machine Learning
Koober Machine Learning
 
Introduction to Machine Learning
Introduction to Machine LearningIntroduction to Machine Learning
Introduction to Machine Learning
 
Salesforce Campus Tour - Developer Intro
Salesforce Campus Tour - Developer IntroSalesforce Campus Tour - Developer Intro
Salesforce Campus Tour - Developer Intro
 
Salesforce Campus Tour - Developer Advanced
Salesforce Campus Tour - Developer AdvancedSalesforce Campus Tour - Developer Advanced
Salesforce Campus Tour - Developer Advanced
 
Salesforce Campus Tour - Declarative
Salesforce Campus Tour - DeclarativeSalesforce Campus Tour - Declarative
Salesforce Campus Tour - Declarative
 
Integrating Clouds & Humans with Wearable Apps
Integrating Clouds & Humans with Wearable AppsIntegrating Clouds & Humans with Wearable Apps
Integrating Clouds & Humans with Wearable Apps
 
Building Reactive Apps
Building Reactive AppsBuilding Reactive Apps
Building Reactive Apps
 
Planet of the AOPs
Planet of the AOPsPlanet of the AOPs
Planet of the AOPs
 

Recently uploaded

JavaLand 2024: Application Development Green Masterplan
JavaLand 2024: Application Development Green MasterplanJavaLand 2024: Application Development Green Masterplan
JavaLand 2024: Application Development Green Masterplan
Miro Wengner
 
TrustArc Webinar - 2024 Global Privacy Survey
TrustArc Webinar - 2024 Global Privacy SurveyTrustArc Webinar - 2024 Global Privacy Survey
TrustArc Webinar - 2024 Global Privacy Survey
TrustArc
 
Energy Efficient Video Encoding for Cloud and Edge Computing Instances
Energy Efficient Video Encoding for Cloud and Edge Computing InstancesEnergy Efficient Video Encoding for Cloud and Edge Computing Instances
Energy Efficient Video Encoding for Cloud and Edge Computing Instances
Alpen-Adria-Universität
 
Taking AI to the Next Level in Manufacturing.pdf
Taking AI to the Next Level in Manufacturing.pdfTaking AI to the Next Level in Manufacturing.pdf
Taking AI to the Next Level in Manufacturing.pdf
ssuserfac0301
 
Public CyberSecurity Awareness Presentation 2024.pptx
Public CyberSecurity Awareness Presentation 2024.pptxPublic CyberSecurity Awareness Presentation 2024.pptx
Public CyberSecurity Awareness Presentation 2024.pptx
marufrahmanstratejm
 
Trusted Execution Environment for Decentralized Process Mining
Trusted Execution Environment for Decentralized Process MiningTrusted Execution Environment for Decentralized Process Mining
Trusted Execution Environment for Decentralized Process Mining
LucaBarbaro3
 
Overcoming the PLG Trap: Lessons from Canva's Head of Sales & Head of EMEA Da...
Overcoming the PLG Trap: Lessons from Canva's Head of Sales & Head of EMEA Da...Overcoming the PLG Trap: Lessons from Canva's Head of Sales & Head of EMEA Da...
Overcoming the PLG Trap: Lessons from Canva's Head of Sales & Head of EMEA Da...
saastr
 
Let's Integrate MuleSoft RPA, COMPOSER, APM with AWS IDP along with Slack
Let's Integrate MuleSoft RPA, COMPOSER, APM with AWS IDP along with SlackLet's Integrate MuleSoft RPA, COMPOSER, APM with AWS IDP along with Slack
Let's Integrate MuleSoft RPA, COMPOSER, APM with AWS IDP along with Slack
shyamraj55
 
zkStudyClub - LatticeFold: A Lattice-based Folding Scheme and its Application...
zkStudyClub - LatticeFold: A Lattice-based Folding Scheme and its Application...zkStudyClub - LatticeFold: A Lattice-based Folding Scheme and its Application...
zkStudyClub - LatticeFold: A Lattice-based Folding Scheme and its Application...
Alex Pruden
 
Driving Business Innovation: Latest Generative AI Advancements & Success Story
Driving Business Innovation: Latest Generative AI Advancements & Success StoryDriving Business Innovation: Latest Generative AI Advancements & Success Story
Driving Business Innovation: Latest Generative AI Advancements & Success Story
Safe Software
 
5th LF Energy Power Grid Model Meet-up Slides
5th LF Energy Power Grid Model Meet-up Slides5th LF Energy Power Grid Model Meet-up Slides
5th LF Energy Power Grid Model Meet-up Slides
DanBrown980551
 
WeTestAthens: Postman's AI & Automation Techniques
WeTestAthens: Postman's AI & Automation TechniquesWeTestAthens: Postman's AI & Automation Techniques
WeTestAthens: Postman's AI & Automation Techniques
Postman
 
Introduction of Cybersecurity with OSS at Code Europe 2024
Introduction of Cybersecurity with OSS  at Code Europe 2024Introduction of Cybersecurity with OSS  at Code Europe 2024
Introduction of Cybersecurity with OSS at Code Europe 2024
Hiroshi SHIBATA
 
Fueling AI with Great Data with Airbyte Webinar
Fueling AI with Great Data with Airbyte WebinarFueling AI with Great Data with Airbyte Webinar
Fueling AI with Great Data with Airbyte Webinar
Zilliz
 
“Temporal Event Neural Networks: A More Efficient Alternative to the Transfor...
“Temporal Event Neural Networks: A More Efficient Alternative to the Transfor...“Temporal Event Neural Networks: A More Efficient Alternative to the Transfor...
“Temporal Event Neural Networks: A More Efficient Alternative to the Transfor...
Edge AI and Vision Alliance
 
System Design Case Study: Building a Scalable E-Commerce Platform - Hiike
System Design Case Study: Building a Scalable E-Commerce Platform - HiikeSystem Design Case Study: Building a Scalable E-Commerce Platform - Hiike
System Design Case Study: Building a Scalable E-Commerce Platform - Hiike
Hiike
 
Generating privacy-protected synthetic data using Secludy and Milvus
Generating privacy-protected synthetic data using Secludy and MilvusGenerating privacy-protected synthetic data using Secludy and Milvus
Generating privacy-protected synthetic data using Secludy and Milvus
Zilliz
 
Columbus Data & Analytics Wednesdays - June 2024
Columbus Data & Analytics Wednesdays - June 2024Columbus Data & Analytics Wednesdays - June 2024
Columbus Data & Analytics Wednesdays - June 2024
Jason Packer
 
Your One-Stop Shop for Python Success: Top 10 US Python Development Providers
Your One-Stop Shop for Python Success: Top 10 US Python Development ProvidersYour One-Stop Shop for Python Success: Top 10 US Python Development Providers
Your One-Stop Shop for Python Success: Top 10 US Python Development Providers
akankshawande
 
Skybuffer AI: Advanced Conversational and Generative AI Solution on SAP Busin...
Skybuffer AI: Advanced Conversational and Generative AI Solution on SAP Busin...Skybuffer AI: Advanced Conversational and Generative AI Solution on SAP Busin...
Skybuffer AI: Advanced Conversational and Generative AI Solution on SAP Busin...
Tatiana Kojar
 

Recently uploaded (20)

JavaLand 2024: Application Development Green Masterplan
JavaLand 2024: Application Development Green MasterplanJavaLand 2024: Application Development Green Masterplan
JavaLand 2024: Application Development Green Masterplan
 
TrustArc Webinar - 2024 Global Privacy Survey
TrustArc Webinar - 2024 Global Privacy SurveyTrustArc Webinar - 2024 Global Privacy Survey
TrustArc Webinar - 2024 Global Privacy Survey
 
Energy Efficient Video Encoding for Cloud and Edge Computing Instances
Energy Efficient Video Encoding for Cloud and Edge Computing InstancesEnergy Efficient Video Encoding for Cloud and Edge Computing Instances
Energy Efficient Video Encoding for Cloud and Edge Computing Instances
 
Taking AI to the Next Level in Manufacturing.pdf
Taking AI to the Next Level in Manufacturing.pdfTaking AI to the Next Level in Manufacturing.pdf
Taking AI to the Next Level in Manufacturing.pdf
 
Public CyberSecurity Awareness Presentation 2024.pptx
Public CyberSecurity Awareness Presentation 2024.pptxPublic CyberSecurity Awareness Presentation 2024.pptx
Public CyberSecurity Awareness Presentation 2024.pptx
 
Trusted Execution Environment for Decentralized Process Mining
Trusted Execution Environment for Decentralized Process MiningTrusted Execution Environment for Decentralized Process Mining
Trusted Execution Environment for Decentralized Process Mining
 
Overcoming the PLG Trap: Lessons from Canva's Head of Sales & Head of EMEA Da...
Overcoming the PLG Trap: Lessons from Canva's Head of Sales & Head of EMEA Da...Overcoming the PLG Trap: Lessons from Canva's Head of Sales & Head of EMEA Da...
Overcoming the PLG Trap: Lessons from Canva's Head of Sales & Head of EMEA Da...
 
Let's Integrate MuleSoft RPA, COMPOSER, APM with AWS IDP along with Slack
Let's Integrate MuleSoft RPA, COMPOSER, APM with AWS IDP along with SlackLet's Integrate MuleSoft RPA, COMPOSER, APM with AWS IDP along with Slack
Let's Integrate MuleSoft RPA, COMPOSER, APM with AWS IDP along with Slack
 
zkStudyClub - LatticeFold: A Lattice-based Folding Scheme and its Application...
zkStudyClub - LatticeFold: A Lattice-based Folding Scheme and its Application...zkStudyClub - LatticeFold: A Lattice-based Folding Scheme and its Application...
zkStudyClub - LatticeFold: A Lattice-based Folding Scheme and its Application...
 
Driving Business Innovation: Latest Generative AI Advancements & Success Story
Driving Business Innovation: Latest Generative AI Advancements & Success StoryDriving Business Innovation: Latest Generative AI Advancements & Success Story
Driving Business Innovation: Latest Generative AI Advancements & Success Story
 
5th LF Energy Power Grid Model Meet-up Slides
5th LF Energy Power Grid Model Meet-up Slides5th LF Energy Power Grid Model Meet-up Slides
5th LF Energy Power Grid Model Meet-up Slides
 
WeTestAthens: Postman's AI & Automation Techniques
WeTestAthens: Postman's AI & Automation TechniquesWeTestAthens: Postman's AI & Automation Techniques
WeTestAthens: Postman's AI & Automation Techniques
 
Introduction of Cybersecurity with OSS at Code Europe 2024
Introduction of Cybersecurity with OSS  at Code Europe 2024Introduction of Cybersecurity with OSS  at Code Europe 2024
Introduction of Cybersecurity with OSS at Code Europe 2024
 
Fueling AI with Great Data with Airbyte Webinar
Fueling AI with Great Data with Airbyte WebinarFueling AI with Great Data with Airbyte Webinar
Fueling AI with Great Data with Airbyte Webinar
 
“Temporal Event Neural Networks: A More Efficient Alternative to the Transfor...
“Temporal Event Neural Networks: A More Efficient Alternative to the Transfor...“Temporal Event Neural Networks: A More Efficient Alternative to the Transfor...
“Temporal Event Neural Networks: A More Efficient Alternative to the Transfor...
 
System Design Case Study: Building a Scalable E-Commerce Platform - Hiike
System Design Case Study: Building a Scalable E-Commerce Platform - HiikeSystem Design Case Study: Building a Scalable E-Commerce Platform - Hiike
System Design Case Study: Building a Scalable E-Commerce Platform - Hiike
 
Generating privacy-protected synthetic data using Secludy and Milvus
Generating privacy-protected synthetic data using Secludy and MilvusGenerating privacy-protected synthetic data using Secludy and Milvus
Generating privacy-protected synthetic data using Secludy and Milvus
 
Columbus Data & Analytics Wednesdays - June 2024
Columbus Data & Analytics Wednesdays - June 2024Columbus Data & Analytics Wednesdays - June 2024
Columbus Data & Analytics Wednesdays - June 2024
 
Your One-Stop Shop for Python Success: Top 10 US Python Development Providers
Your One-Stop Shop for Python Success: Top 10 US Python Development ProvidersYour One-Stop Shop for Python Success: Top 10 US Python Development Providers
Your One-Stop Shop for Python Success: Top 10 US Python Development Providers
 
Skybuffer AI: Advanced Conversational and Generative AI Solution on SAP Busin...
Skybuffer AI: Advanced Conversational and Generative AI Solution on SAP Busin...Skybuffer AI: Advanced Conversational and Generative AI Solution on SAP Busin...
Skybuffer AI: Advanced Conversational and Generative AI Solution on SAP Busin...
 

Kotlin Mullets

  • 1. Kotlin Mullets Business on the Front End, Party in Back Chet Haase, Developer Advocate, Android James Ward, Developer Advocate, Google Cloud
  • 3. Track ● Track Motion Events Record ● Add Locations to Path(s) View ● Invalidate ML ● Ready to Analyze Draw ● Draw Path Data User Drawings
  • 4. UI Layout <androidx.constraintlayout.widget.ConstraintLayout ...> <Spinner ... /> <Button ... /> <Button ... /> <ToggleButton... /> <Button ... /> <TextView... /> <TextView ... /> <TextView ... /> <TextView ... /> <com.jamesward.airdraw.DrawingCanvas ... /> </androidx.constraintlayout.widget.ConstraintLayout>
  • 5. UI Layout, with custom view <androidx.constraintlayout.widget.ConstraintLayout ...> < ... /> <com.jamesward.airdraw.DrawingCanvas android:id="@+id/drawingCanvas" android:layout_width="0dp" android:layout_height="0dp" android:layout_margin="8dp" android:background="@android:color/black" app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toEndOf="@id/rightGuide" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="@id/leftGuide" app:layout_constraintTop_toBottomOf="@+id/fourthGuess" /> </androidx.constraintlayout.widget.ConstraintLayout>
  • 6. DrawingCanvas class DrawingCanvas(context: Context?, attr: AttributeSet) : View(context, attr) { val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) private val drawingPath = Path() fun getBitmap():Bitmap { ... } fun setBitmap(bitmap: Bitmap) { ... } override fun onTouchEvent(event: MotionEvent?): Boolean { ... } fun clear() { ... } override fun onDraw(canvas: Canvas?) { ... } }
  • 7. Track Motion Events override fun onTouchEvent(event: MotionEvent?): Boolean { var handled = false if (event != null) { when { event.action == MotionEvent.ACTION_DOWN -> { drawingPath.moveTo(event.x, event.y) drawingPath.lineTo(event.x, event.y) invalidate() handled = true } event.action == MotionEvent.ACTION_MOVE -> { drawingPath.lineTo(event.x, event.y) invalidate() handled = true } event.action == MotionEvent.ACTION_UP -> { handled = true } } } return handled }
  • 8. Draw Path override fun onDraw(canvas: Canvas?) { canvas?.drawPath(drawingPath, paint) }
  • 9. Interlude: Learning About Machine Learning Tensor FlowTraining Data Model TFLite Model On-Device Model User Data Results
  • 11. Or…. Use Built-in Models Tensor FlowTraining Data Model TFLite Model On-Device Model User Data Results
  • 12. Convert Drawing ● Extract Bitmap Setup Labeler ● Create Firebase Vision Image ● Create On- Device Labeler Send Data ● Process Data Receive Results ● onSuccess callback receives results Get Image Labels Display Results ● Show Labels and Confidence
  • 13. Get Bitmap Data fun getBitmap():Bitmap { val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) draw(canvas) return bitmap }
  • 14. KTX
  • 15. Get Bitmap Data fun getBitmap():Bitmap { val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) draw(canvas) return bitmap }
  • 16. Get Bitmap Data fun getBitmap() = createBitmap(width, height).applyCanvas(::draw) fun getBitmap():Bitmap { val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) draw(canvas) return bitmap }
  • 17. Call Image Labeler val firebaseImage = FirebaseVisionImage.fromBitmap(bitmap) val labeler = FirebaseVision.getInstance().onDeviceImageLabeler labeler.processImage(firebaseImage).addOnSuccessListener { // . . . } { {“label”, confidence}, {“label”, confidence}, … }
  • 18. Digital Processing: MNIST Image: ©Josef Steppan, https://commons.wikimedia.org/wiki/File:MnistExamples.png Modified National Institute of Standards and Technology Hand-written digits for image-processing
  • 19. Create Interpreter ● Load model ● Create Interpreter with model Get Bitmap ● Get user data as bitmap ● Resize to match model Run Interpreter ● Process data Process Results ● Sort results by confidence Get Image Labels Display Results ● Show results and confidence
  • 20. Get Model as ByteBuffer val model = loadModelFile() private fun loadModelFile(): ByteBuffer { val fileDescriptor = assets.openFd("mnist.tflite") val inputStream = FileInputStream(fileDescriptor.fileDescriptor) val fileChannel = inputStream.channel val startOffset = fileDescriptor.startOffset val declaredLength = fileDescriptor.declaredLength return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength) }
  • 21. Initialize Interpreter, Sizes val options = Interpreter.Options() options.setUseNNAPI(true) val interpreter = Interpreter(model, options) val inputShape = interpreter.getInputTensor(0).shape() inputImageWidth = inputShape[1] inputImageHeight = inputShape[2] modelInputSize = 4 * inputImageWidth * inputImageHeight
  • 22. Get Results private fun detectDigit(bitmap: Bitmap, resultsListener: (List<LabelAnnotation>) -> Unit) { lateinit var digitSequence: List<Pair<String,Float>> val resizedImage = Bitmap.createScaledBitmap(bitmap, inputImageWidth, inputImageHeight, true) val byteBuffer = convertBitmapToByteBuffer(resizedImage) val result = Array(1) { FloatArray(10) } interpreter.run(byteBuffer, result) // format and display results }
  • 23. Demo!
  • 24. KTS
  • 25. Type-safe Builds dependencies { implementation(kotlin("stdlib-jdk8")) implementation("androidx.appcompat:appcompat:1.1.0") ... } build.gradle.kts fun Project.dependencies(configuration: DependencyHandlerScope.() -> Unit) = DependencyHandlerScope.of(dependencies).configuration() fun DependencyHandler.`implementation`(dependencyNotation: Any): Dependency? = add("implementation", dependencyNotation)
  • 26. Declarative(ish) & Imperative if (startParameter.taskRequests.find { it.args.contains(":server:shadowJar") } == null) { include("common", "android", "web", "server") } else { include("common", "web", "server") } settings.gradle.kts
  • 27. ML in the Cloud val firebaseImage = FirebaseVisionImage.fromBitmap(bitmap) val labeler = FirebaseVision.getInstance().onDeviceImageLabeler labeler.processImage(firebaseImage).addOnSuccessListener { … } On-Device Labeling
  • 28. ML in the Cloud val firebaseImage = FirebaseVisionImage.fromBitmap(bitmap) val labeler = FirebaseVision.getInstance().cloudImageLabeler labeler.processImage(firebaseImage).addOnSuccessListener { … } Cloud Labeling
  • 29. Demo!
  • 31. Get Results private fun detectDigit(bitmap: Bitmap, resultsListener: (List<LabelAnnotation>) -> Unit) { lateinit var digitSequence: List<Pair<String,Float>> val resizedImage = Bitmap.createScaledBitmap(bitmap, inputImageWidth, inputImageHeight, true) val byteBuffer = convertBitmapToByteBuffer(resizedImage) val result = Array(1) { FloatArray(10) } interpreter.run(byteBuffer, result) // format and display results }
  • 32. Get Results private fun detectDigit(bitmap: Bitmap, resultsListener: (List<LabelAnnotation>) -> Unit) { lateinit var digitSequence: List<Pair<String,Float>> val resizedImage = Bitmap.createScaledBitmap(bitmap, inputImageWidth, inputImageHeight, true) val byteBuffer = convertBitmapToByteBuffer(resizedImage) val result = Array(1) { FloatArray(10) } interpreter.run(byteBuffer, result) // format and display results }
  • 33. Get Results private suspend fun detectDigit(bitmap: Bitmap, resultsListener: (List<LabelAnnotation>) -> Unit) { lateinit var digitSequence: List<Pair<String,Float>> val resizedImage = Bitmap.createScaledBitmap(bitmap, inputImageWidth, inputImageHeight, true) val byteBuffer = convertBitmapToByteBuffer(resizedImage) val result = Array(1) { FloatArray(10) } withContext(Dispatchers.IO) { interpreter.run(byteBuffer, result) } // format and display results }
  • 35.
  • 36. Draw ● Capture Bitmap Phone ML ● Predict with Local Model ● Display Results REST Service ● Send Bitmap & Results to REST API Browser ● Poll d' Bus ● Display Bitmap & Results Pub/Sub ● On d' Bus Browser Bus Server
  • 37. Shared Common Root Gradle Project Common ServerAndroid data class LabelAnnotation( val description: String, val score: Float) data class ImageResult( val image: ByteArray, val labelAnnotations: List<LabelAnnotation>) common/src/commonMain/kotlin/Data.kt
  • 38. Multiplatform Common plugins { id("com.android.library") kotlin("multiplatform") } java { sourceCompatibility = JavaVersion.VERSION_1_8 } android { defaultConfig { compileSdkVersion(29) } } common/build.gradle.ktx
  • 39. Multiplatform Common kotlin { sourceSets { commonMain { dependencies { implementation(kotlin("stdlib")) } } } jvm { val main by compilations.getting { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8 } } } android() } common/build.gradle.ktx
  • 40. Multiplatform Common Dependency dependencies { implementation(project(":common")) } android/build.gradle.ktx server/build.gradle.kts
  • 41.
  • 42. Cloud Service - Deps plugins { kotlin("kapt") } dependencies { implementation("io.micronaut:micronaut-runtime:1.2.6") implementation("io.micronaut:micronaut-http-server-netty:1.2.6") implementation("io.micronaut:micronaut-views:1.2.0") api("ch.qos.logback:logback-classic:1.2.3") api("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.1") api("org.thymeleaf:thymeleaf:3.0.11.RELEASE") kapt("io.micronaut:micronaut-inject-java:1.2.6") } server/build.gradle.ktx
  • 43. Cloud Service @Post("/show") fun show(@Body imageResult: ImageResult): HttpResponse<String> { bus.put(imageResult) return HttpResponse.ok("") } @Get("/events") fun events(): HttpResponse<ImageResult> { val maybe = bus.take() return if (maybe != null) HttpResponse.ok(maybe) else HttpResponse.noContent() } server/src/main/kotlin/WebApp.kt
  • 44. Client @Client("${drawurl}") interface DrawService { @Post("/show") fun show(@Body imageResult: ImageResult): Single<Unit> } @Inject var drawService: DrawService? = null drawService?.show(imageResult)?.subscribe()?.dispose() android/src/main/kotlin/MainActivity.kt
  • 45. Setup DI class BaseApplication: Application() { private var ctx: ApplicationContext? = null override fun onCreate() { super.onCreate() val pm = applicationContext.packageManager val ai = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA) val propertySource = AndroidMetadataPropertySource(ai.metaData) val appCtxBuilder = applicationContext.build(MainActivity::class.java, Environment.ANDROID) ctx = appCtxBuilder.propertySources(propertySource).start() registerActivityLifecycleCallbacks(object: ActivityLifecycleCallbacks { override fun onActivityCreated(activity: Activity?, bundle: Bundle?) { if (activity != null) ctx?.inject(activity) } }) } android/src/main/kotlin/BaseApplication.kt
  • 46. Config via AndroidManfest.xml <meta-data android:name="drawurl" android:value="${drawurl}"/> val drawUrl: String? by project if (drawUrl != null) { manifestPlaceholders = mapOf("drawurl" to drawUrl) } else { // 10.0.2.2 is the IP for your machine from the Android emulator manifestPlaceholders = mapOf("drawurl" to "http://10.0.2.2:8080") } android/build.gradle.ktx android/src/main/AndroidManifest.xml drawUrl=http://192.168.1.23:8080 gradle.properties
  • 47. Cloud Run, Run FROM adoptopenjdk/openjdk8 as builder WORKDIR /app COPY . /app RUN ./gradlew --no-daemon --console=plain :server:shadowJar FROM adoptopenjdk/openjdk8:jre COPY --from=builder /app/server/build/libs/server.jar /server.jar RUN apt-get update && apt-get install -y --no-install-recommends fontconfig CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/server.jar"] Dockerfile
  • 48. Build & Deploy steps: - name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', 'gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA', '/workspace'] - name: 'gcr.io/cloud-builders/docker' args: ['push', 'gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA'] - name: 'gcr.io/cloud-builders/gcloud' args: ['beta', 'run', 'deploy', '--image=gcr.io/$PROJECT_ID/$REPO_NAME:$COMMIT_SHA', '--platform=managed', '--project=$PROJECT_ID', '--region=us-central1', '--allow- unauthenticated', '--memory=512Mi', '$REPO_NAME'] cloudbuild.yaml
  • 49. Demo!
  • 51. Jetpack Compose Reactive, declarative UI Toolkit Kotlin-first Developer Preview (Pre-Alpha)
  • 52. Demo!
  • 53. Track ● Track Motion Events Record ● Add Locations to Path(s) View ● Invalidate ML ● Ready to Analyze Draw ● Draw Path Data User Drawings In Compose!
  • 54. UI Layout @Composable fun BuildUI(guesses: Guesses = Guesses()) { MaterialTheme() { val invalidator = +invalidate Column(Spacing(8.dp), crossAxisAlignment = CrossAxisAlignment.Stretch) { FlexRow(mainAxisAlignment = MainAxisAlignment.Center) { val radioOptions = listOf("Shape", "Digit") val (selectedOption, onOptionSelected) = +state { radioOptions[0] } inflexible { RadioGroup( options = radioOptions, selectedOption = selectedOption, onSelectedChange = onOptionSelected ) } flexible(1f) { Column(crossAxisAlignment = CrossAxisAlignment.Stretch) { Button(text = "Local", ...) Button(text = "Cloud", ...) } } } Button(text = "Sensorify", ...) HeightSpacer(8.dp) GuessDisplay(guesses) HeightSpacer(8.dp) DrawingCanvas(path) HeightSpacer(8.dp) Button(text = "Clear", ...) HeightSpacer(8.dp) } } }
  • 55. UI Layout @Composable fun BuildUI(guesses: Guesses = Guesses()) { MaterialTheme() { val invalidator = +invalidate Column(Spacing(8.dp), crossAxisAlignment = CrossAxisAlignment.Stretch) { FlexRow(mainAxisAlignment = MainAxisAlignment.Center) { val radioOptions = listOf("Shape", "Digit") val (selectedOption, onOptionSelected) = +state { radioOptions[0] } inflexible { RadioGroup( options = radioOptions, selectedOption = selectedOption, onSelectedChange = onOptionSelected ) } flexible(1f) { Column(crossAxisAlignment = CrossAxisAlignment.Stretch) { Button(text = "Local", ...) Button(text = "Cloud", ...) } } } Button(text = "Sensorify", ...) HeightSpacer(8.dp) GuessDisplay(guesses) HeightSpacer(8.dp) DrawingCanvas(path) HeightSpacer(8.dp) Button(text = "Clear", ...) HeightSpacer(8.dp) } } }
  • 56. DrawingCanvas @Composable fun DrawingCanvas(path: Path) { val invalidate = +invalidate RawDragGestureDetector(dragObserver = MyDragObserver(path, invalidate)) { Surface(color = Color.Black) { Container(modifier = ExpandedHeight, width = 200.dp, height = 350.dp) { Draw { canvas, parentSize -> canvas.drawPath(path, fingerPaint) } } } } }
  • 57. DrawingCanvas @Composable fun DrawingCanvas(path: Path) { val invalidate = +invalidate RawDragGestureDetector(dragObserver = MyDragObserver(path, invalidate)) { Surface(color = Color.Black) { Container(modifier = ExpandedHeight, width = 200.dp, height = 350.dp) { Draw { canvas, parentSize -> canvas.drawPath(path, fingerPaint) } } } } }
  • 58. Track Motion Events class MyDragObserver(val dragPath: Path, val recompose: () -> Unit): DragObserver { override fun onStart(downPosition: PxPosition) { dragPath.moveTo(downPosition.x.value, downPosition.y.value) } override fun onDrag(dragDistance: PxPosition): PxPosition { dragPath.relativeLineTo(dragDistance.x.value, dragDistance.y.value) recompose() return dragDistance } }
  • 59. Machine Learning Code Button(text = "Local", onClick = { machineLearningStuff.detectObject(true, bitmap!!, selectedOption == "Shape") { // display results in Text objects... } })
  • 61. Shared Common Root Gradle Project Common ServerAndroid Web Jar
  • 62. Kotlin JS - Build plugins { kotlin("js") } dependencies { implementation(kotlin("stdlib-js")) implementation(project(":common")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.2") implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.6.12") } web/build.gradle.kts
  • 63. Kotlin JS - Assemble JsJar tasks.withType<org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile> { kotlinOptions { metaInfo = false sourceMap = true outputFile = "$buildDir/classes/kotlin/main/${project.name}.js" } } task<Copy>("assembleJsLib") { from(...) into("$buildDir/classes/kotlin/main") dependsOn("mainClasses") } tasks { JsJar { into("META-INF/resources") dependsOn("assembleJsLib") } } web/build.gradle.kts
  • 64. Kotlin JS - Polling fun main() { GlobalScope.launch { poll() } } suspend fun poll() { val res = window.fetch("/events").await() when (res.status.toInt()) { 200 -> { val json = res.json().await().asDynamic() ... val imageResult = ImageResult(byteArray, labelAnnotations) ... } } window.setTimeout({ GlobalScope.launch { poll() } }, 1000) } web/src/main/kotlin/Main.kt
  • 65. Kotlin JS - Rendering val urlImage = "url('data:image/png;base64,${imageResult.image.decodeToString()}')" document.body?.style?.backgroundImage = urlImage document.body?.clear() val div = document.create.div() imageResult.labelAnnotations.forEach { labelAnnotation -> div.append { p { +"${labelAnnotation.description} = ${round(labelAnnotation.score * 100)}%" } } } document.body?.append(div) web/src/main/kotlin/Main.kt
  • 66. Kotlin JS - Serving Static Resources micronaut.router.static-resources.resources.enabled=true micronaut.router.static-resources.resources.paths=classpath:META-INF/resources micronaut.router.static-resources.resources.mapping=/resources/** server/src/main/resources/application.properties dependencies { api(files("../web/build/libs/web.jar")) } tasks.withType<org.jetbrains.kotlin.gradle.internal.KaptWithKotlincTask> { dependsOn(":web:JsJar") } server/build.gradle.kts
  • 67. Kotlin JS - HTML Page <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Air Draw</title> <link rel="stylesheet" href="assets/index.css"> <script src="resources/kotlin.js"></script> <script src="resources/kotlinx-coroutines-core.js"></script> <script src="resources/kotlinx-html-js.js"></script> <script src="resources/kotlinx-serialization-kotlinx-serialization- runtime.js"></script> <script src="resources/common.js"></script> <script src="resources/web.js"></script> </head> <body> waiting for drawings... </body> </html> server/src/main/kotlin/resources/views/index.html
  • 69. One Project To Kotlin Them All Root Gradle Project Common ServerAndroid Web build.gradle.kts common/build.gradle.kts android/build.gradle.kts server/build.gradle.kts web/build.gradle.kts
  • 70. One Project To Rule Them All buildscript { repositories { mavenLocal() mavenCentral() jcenter() google() } dependencies { classpath(kotlin("gradle-plugin", "1.3.61")) classpath("com.android.tools.build:gradle:4.0.0-alpha04") } } build.gradle.kts (Root Gradle Project)
  • 71. Move Data Processing to the Server
  • 72. Demo!
  • 73. Air Draw ● Capture Sensor Data REST Service ● Smoooooth Data ● Render to Image Cloud ML ● Send Bitmap to Cloud ML ● Send Bitmap & results to Phone Browser ● Poll d' Bus ● Display Bitmap & Results Pub/Sub ● On d' Bus
  • 74. Mapping Sensor Data to an Image
  • 75. Sensors & Vision data class Orientation(val azimuth: Float, val pitch: Float, val timestamp: Long) SensorManager.getRotationMatrixFromVector(rotationMatrix, e.values) val m = FloatArray(3) SensorManager.getOrientation(rotationMatrix, orientationAngles) val orientation = Orientation(m[0], m[1], e.timestamp) readings.add(orientation) common/src/commonMain/kotlin/Data.kt
  • 76. SEND d' DATA! @Client("${drawurl}") interface DrawService { @Post("/draw") fun draw(@Body readings: List<Orientation>): Single<ImageResult> } machineLearningStuff.sensorAction(on) { orientations -> val imageResultMaybe = drawService?.draw(orientations)?.blockingGet() imageResultMaybe?.let { imageResult -> displayResults(imageResult) } } android/src/main/kotlin/MainActivity.kt
  • 77. RECEIVE d’ DATA! @Post("/draw") fun draw(@Body readingsSingle: List<Orientation>): Single<ImageResult> { return readingsSingle.map { readings -> airDraw.run(readings)?.let { imageResult -> bus.put(imageResult) HttpResponse.ok(imageResult) } } } server/src/main/kotlin/WebApp.kt
  • 78. Things get crazy val xl = KrigingInterpolation1D(t, x) ... import java.awt.image.BufferedImage val bi = BufferedImage(canvas.width, canvas.height, BufferedImage.TYPE_INT_ARGB) ... val imgBytes = ByteString.copyFrom(bytes) val img = Image.newBuilder().setContent(imgBytes).build() val feature = Feature.newBuilder().setType(Type.LABEL_DETECTION).build() val request = AnnotateImageRequest.newBuilder().addFeatures(feature).setImage(img).build() val r = myImageAnnotatorClient.imageAnnotatorClient.batchAnnotateImages(arrayListOf(request)) server/src/main/kotlin/WebApp.kt
  • 79. Pitfalls ● Gradle Build Plugins ● JSON ● Externalizing the URL ● Math is hard ● ML is hard ● Compose is Young

Editor's Notes

  1. emulator: drawing, local shape detection
  2. User Data: pictures or video
  3. User Data: pictures or video
  4. KTX
  5. KTS: Kotlin Scripting
  6. Same as on-device labeler, but different setup
  7. Same as on-device labeler, but different setup
  8. Same as on-device labeler, but different setup
  9. Same as on-device labeler, but different setup
  10. Coroutines
  11. processing can take several frames. Or for firebase/cloud calls, more than a second.
  12. Server Side
  13. Explain mullet
  14. Upload the Image & Labels to an app so we can share them
  15. Upload the Image & Labels to an app so we can share them
  16. Upload the Image & Labels to an app so we can share them
  17. Upload the Image & Labels to an app so we can share them
  18. Upload the Image & Labels to an app so we can share them
  19. Upload the Image & Labels to an app so we can share them
  20. Upload the Image & Labels to an app so we can share them
  21. Upload the Image & Labels to an app so we can share them
  22. Jetpack Compose
  23. UI code builds hierarchy given current state NOT using compose compiler
  24. Note similar canvas drawing code
  25. difference in how we get/cache the bitmap, but same function calls from withing button click listeners
  26. Web UI
  27. Multi-Module Kotlin Project
  28. Moving Heavy Data Processing to the Server