Skip to main content

113 posts tagged with "Android"

View All Tags

Applying Systrace for Low-Level Performance Tuning in Android Apps

Published: · 6 min read
Andrea Sunny
Marketing Associate, Appxiom

Introduction: The Unseen Cost of Poor Mobile Performance

In mobile development, app speed and reliability aren't luxuries-they're the price of entry. Even small performance issues-UI jank, input lag, or unresponsive screens-directly translate to user churn and negative reviews. For engineers, these aren’t “just bugs”; they are signals of deeper systemic issues, often buried within OS layers and not easily uncovered with surface-level profiling.

This is where Systrace steps in. Far from a basic profiling tool, Systrace delivers deep, OS-level observability, empowering developers, QA engineers, and engineering leaders to find the real root causes of performance cliffs in Android apps. In this post, we’ll dive into how to leverage Systrace for actionable low-level performance tuning, focusing on practical debugging, observability, and reliability strategies for all skill levels.

Why Systrace: Beyond Studio Profiler and Logcat

While tools like Android Studio Profiler and Logcat provide critical insights, their granularity often ends at the process or framework level. Issues like “mysterious” jank, dropped frames, background thread bottlenecks, or cross-process contention often stay hidden. Systrace fills these gaps by capturing a system-wide, time-stamped trace of what every thread and process (including the kernel and system services) are doing.

Common real-world issues Systrace helps uncover:

  • UI thread blocked on I/O, mistakenly assumed to be a CPU bottleneck
  • Long GC (Garbage Collection) pauses causing animation stutter
  • Synchronization deadlocks or lock contention on background workers
  • Misuse of the main thread for expensive operations (leading to ANR)
  • Resource contention between your app and system daemons

Key Systrace Features:

  • Visualizes thread states and events over time
  • Points directly to which code or system resource is the true bottleneck
  • Offers microsecond-level temporal accuracy

Using Systrace in Practice: Step-by-Step Workflow

1. Setup and Capture a Trace

Systrace is available via adb or the Android Studio Profiler:

adb shell 'setprop debug.atrace.tags.enableflags 0xFFFFFFFF'
adb shell atrace -z -b 4096 -t 10 gfx view wm input sched freq idle am res dalvik > trace.html
  • -t 10 captures 10 seconds.
  • The event categories (gfx, view, etc.) control which subsystems to trace.
  • Output is a self-contained HTML for Chrome’s trace viewer.

Pro tip: Always capture a few seconds before and after the incident. Many performance problems are effects, not causes.

2. Reading the Trace: Key Patterns to Spot

Load trace.html in Chrome. Here’s what to look for:

  • Jank & Frame Drops: Look for long red blocks or gaps in Choreographer, RenderThread or MainThread bars.
  • Long CPU Burst: Examine “sched” lanes; excessive CPU time on main/UI thread can signal unoptimized code.
  • Blocking on I/O or Locks: “Uninterruptible sleep” or “mutex_wait” in thread state-a sign your UI thread is waiting for disk/network or locks.
  • GC Events: GC activity (seen as “GC” or “Dalvik” events) overlapping frame rendering often correlates with visible UI stutter.

Actionable Debugging: Practical Examples

Let’s explore concrete scenarios and how Systrace provides answers where other tools fall short.

Case 1: UI Jank on List Scrolling

Problem: Users report laggy scrolling when images load in a RecyclerView.

With Systrace:

  • You see MainThread blocked for ~60ms, coinciding perfectly with dequeueBuffer in RenderThread.
  • Zooming in, you spot “disk_read” in a worker thread initiated by the image loader, but a lock contention with the main thread.

Root Cause: The image loader’s result is being posted synchronously back to the UI thread, causing it to wait unnecessarily.

Solution: Refactor to fully decouple image loading and UI update, perhaps via AsyncListDiffer or separate UI handler.

Case 2: Random, Infrequent ANRs (App Not Responding)

Problem: Sporadic ANRs in production with no clear thread in ANR reports.

With Systrace:

  • You find that several background threads are hitting heavy disk I/O at the same time the main thread tries to commit SharedPreferences synchronously.
  • The “sched” lane shows the main thread is runnable but not scheduled-starved by system load.

Root Cause: Too many concurrent background jobs are blocking system-level I/O.

Solution: Batch writes, use apply() for async SharedPreferences commits, and set sensible thread pool limits.

Building Observability Into Your App: Making Systrace Even Stronger

Systrace supports custom trace markers. Annotate critical parts of your code to trace business logic, not just framework operations.

Example: Annotating long-running code

import android.os.Trace

fun loadData() {
Trace.beginSection("LoadData:fetchFromApi") // Custom marker
// Expensive network or DB code here
Trace.endSection()
}

These custom sections become visible in traces, making it much easier to map expensive operations to code changes, releases, and business features.

Tips for actionable observability:

  • Use markers for large DB queries, network calls, and custom rendering.
  • Combine Systrace with app-level logging to correlate user-level events and system-level performance.

Reliability: Preemptive Tuning and Guardrails

Engineering leaders and QA teams can leverage Systrace as a proactive safeguard in release cycles:

  • Baseline creation: Regular Systrace captures from “stable” releases create a performance baseline. Compare traces after major merges to spot regressions before rollout.
  • CI Integration: Automated smoke tests can trigger Systrace captures for key user flows, alerting engineers to invisible performance regressions early.
  • Production forensics: Ship lightweight Systrace collectors (with user opt-in) to capture post-mortem traces for irreproducible bugs.

Takeaways and Next Steps

Systrace is not just another profiling tool-it’s your OS-level microscope for Android performance. By surfacing kernel, framework, and application events side-by-side, it empowers developers and leaders to:

  • Precisely diagnose the source of jank, ANR, or mysterious slowdowns.
  • Implement observability with custom trace markers.
  • Leverage traces to proactively guard reliability across engineering teams.

Action Items:

  • Integrate Systrace captures into your regular performance debugging toolkit-not just for “crash” bugs, but for every major user flow.
  • Start annotating your code with custom markers today for business-relevant observability.
  • Encourage team-wide familiarity with reading and interpreting Systrace outputs as an engineering best practice.

Looking forward: As Android frameworks become more complex and performance expectations rise, deep system observability is not optional. Systrace enables you to build not just faster apps, but fundamentally more reliable and predictable mobile experiences.

Further Reading & Resources:

Stay curious, stay precise-happy tracing!

Memory Management Strategies to Reduce GC Pauses in Android Applications

Published: · 5 min read
Andrea Sunny
Marketing Associate, Appxiom

Introduction

Garbage Collection (GC) pauses are among the most common sources of frame drops, UI jank, and app unresponsiveness in Android applications. For developers, QA engineers, and engineering leaders, understanding and managing memory efficiently is not a mere academic interest-it's the bedrock of delivering smooth, reliable user experiences. In this post, we’ll explore actionable, real-world strategies for optimizing memory usage, reducing GC-induced performance hitches, improving observability, and enhancing app reliability. Whether you’re just starting with Android or leading a team through the trenches of mobile performance, this guide provides technical depth and practical solutions that you can apply immediately.


Understanding the Real-World Impact of GC Pauses

Before diving into optimizations, let’s ground our approach in real-world challenges:

  • GC Pauses Harm Perceived Performance: Even a 100-200ms pause-barely perceptible on paper-can nullify the painstaking work behind silky-smooth animations or lightning-fast data fetches.
  • Memory Leaks = Reliability Nightmares: Unchecked leaks can eventually destabilize the entire app, leading to crashes or system-driven terminations.
  • Mobile Constraints Multiply Trouble: With limited RAM, slower CPUs, and frequent app switching, Android apps feel the sting of poor memory management more acutely than their desktop counterparts.

Performance Optimization: Strategies to Minimize GC Pauses

1. Use Object Pools Carefully

While Java/Kotlin's GC ultimately collects unused objects, object pooling can significantly reduce allocation churn, especially for short-lived, high-frequency objects (e.g., Bitmap, custom buffer types). However, misuse can lead to stale references and leaks.

object ByteArrayPool {
private val pool = ArrayDeque<ByteArray>()

fun acquire(size: Int): ByteArray =
pool.find { it.size == size } ?: ByteArray(size)

fun release(array: ByteArray) {
pool.add(array)
}
}

Tip: Only pool objects with measurable allocation overhead and ensure timely release back to the pool after use.

2. Prefer Value Types and Primitives

Reducing heap allocations for simple data can decrease GC pressure:

  • Use primitive arrays (IntArray, FloatArray) instead of boxed counterparts.
  • Favor Kotlin's data classes judiciously and be aware that excessive nesting creates more GC work.

3. Limit Use of Anonymous Inner Classes and Lambdas in Hot Paths

Each lambda or anonymous inner class may result in an object allocation. In performance-critical sections (adapters, frequent callbacks), prefer function references or static singletons where applicable.

// Instead of:
button.setOnClickListener { /* ... */ }

// Use:
val sharedClickListener = View.OnClickListener { /* ... */ }
button.setOnClickListener(sharedClickListener)

4. Control Bitmap Memory Proactively

  • Always recycle() unused bitmaps.
  • Prefer inBitmap option with BitmapFactory.Options to reuse existing memory.
  • Downsample images based on actual display size.
val options = BitmapFactory.Options().apply {
inSampleSize = calculateSampleSize(...)
inMutable = true
}
BitmapFactory.decodeResource(resources, R.drawable.large_image, options)

5. Defer and Batch Allocations

Whenever feasible, allocate memory-intensive objects off the UI thread and batch allocations to avoid small, frequent heap pressure bursts.


Debugging Memory Issues: Effective Strategies

Tools of the Trade

  • Android Studio Profiler: Inspect heap allocations, GC events, and allocation timelines for your app in real-time.
  • LeakCanary: Automatically detects memory leaks and provides stack traces to frequently-leaked objects.
  • MAT (Memory Analyzer Tool): Analyze heap dump files (.hprof) for deep inspection and dominator tree analysis.

Practical Debugging Steps

  1. Capture Heap Dumps at Key Interactions: Especially after navigating back from a memory-heavy screen, to ensure expected GC and deallocation.
  2. Track Lifecycle-Related Leaks: Fragments, views, and background jobs often outlive their welcome-ensure you sever references in proper lifecycle methods.
override fun onDestroyView() {
super.onDestroyView()
binding = null // Prevents view holding reference in Fragment
}
  1. Instrument and Annotate Code: Use loggers and custom tooling to record object lifecycle events, making it easier to spot trends that lead to memory bloat.

Implementing Observability for Memory Health

Observability isn’t just for server-side systems. Reliable Android apps demand similar discipline.

Expose and Monitor Key Metrics

  • Heap Usage: Sample heap size periodically and log spikes. Android’s Debug.getNativeHeapAllocatedSize() is useful for tracking native memory leaks.
  • GC Frequency and Duration: React to repeated GC cycles (known as GC thrashing) that often precede crashes or OOM errors.
val runtime = Runtime.getRuntime()
Log.d("Memory", "Used: ${(runtime.totalMemory() - runtime.freeMemory()) / 1024} KB")

In-App and Remote Monitoring

  • Integrate lightweight reporting (e.g., via Firebase Performance Monitoring, custom endpoints) to capture memory health in production, not just in dev builds.
  • For QA teams: automate checks for memory peaks during exploratory testing flows.

Ensuring Application Reliability

Reliability means your app remains stable under stress, high user load, and edge conditions.

Defensive Engineering Approaches

  • Graceful Degradation: Catch and report OutOfMemoryError in isolated threads/components; avoid bringing down the whole app.
  • Low-Memory Handling: Leverage ComponentCallbacks2.onTrimMemory() to proactively release caches or scale down memory use when signaled by the OS.
  • Automated Stress Testing: Regularly run soak and stress tests that simulate extended usage, background/foreground rapid switching, and large input datasets.
override fun onTrimMemory(level: Int) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
// Free up in-memory caches, clear bitmap pools, etc.
imageCache.clear()
}
}

Key Takeaways and Next Steps

GC pauses are inevitable, but their impact can be drastically mitigated with disciplined, context-aware memory management. Proactive optimization, rigorous debugging, robust observability, and defensive engineering collectively build critically reliable mobile experiences.

Next steps for your team:

  • Audit object allocation and deallocation patterns-single out hot paths.
  • Instrument your builds with observability and leak detection from early development to production.
  • Encourage “fail early, recover gracefully” approaches to memory errors.
  • Bake memory stress into your automated QA workflows.

Building responsive, crash-free Android apps isn’t just about writing correct code-it’s about owning the full lifecycle of memory within your app, from allocation to deallocation, visibility to resilience. Start small, measure often, and empower your team to deliver apps users can trust-even under pressure.


Further Reading:

Happy debugging and optimizing!

How to Implement the Decorator Pattern in Jetpack Compose

Published: · Last updated: · 3 min read
Don Peter
Cofounder and CTO, Appxiom

How to Implement the Decorator Pattern in Jetpack Compose

Jetpack Compose gives you an incredible amount of freedom when building Android UIs. You describe what the UI should look like, and Compose takes care of the rest. But even with this flexibility, there are moments where you want to add behavior or styling around a component - without rewriting it or making it harder to maintain.

That's where the Decorator Pattern fits in beautifully.

The decorator pattern allows you to wrap additional behavior or visual enhancements around an existing component without changing its core implementation. In Jetpack Compose, this aligns perfectly with composable functions and modifiers, letting you layer responsibilities in a clean, reusable, and scalable way.

How to Test Jetpack Compose UIs Using Espresso

Published: · Last updated: · 5 min read
Don Peter
Cofounder and CTO, Appxiom

UI bugs are sneaky. Everything looks fine on your device, animations feel smooth, and then - someone reports that a button doesn't respond, a screen doesn't load, or a critical flow breaks on a specific device. By the time you hear about it, the damage is already done.

This is where UI testing earns its keep.

With Jetpack Compose becoming the standard way to build Android UIs, testing strategies need to evolve as well. Espresso is still a powerful UI testing tool - but testing Compose-based UIs requires a slightly different mindset.

Let's walk through how to test Jetpack Compose UIs using Espresso, step by step, in a way that actually makes sense when you sit down to write tests.

Prerequisites

Before jumping into writing tests, make sure you have the basics in place:

  • An Android project using Jetpack Compose
  • Android Studio Arctic Fox or newer
  • Basic familiarity with:
    • Jetpack Compose
    • Espresso
    • JUnit
  • UI tests enabled in your project (androidTest source set)

If you already have a Compose screen running, you're good to go.

Setting Up Espresso for a Compose Project

Jetpack Compose doesn't replace Espresso - it complements it. Espresso still handles UI synchronization and assertions, while Compose provides its own testing APIs.

In your app module, make sure you have the required dependencies:

androidTestImplementation 'androidx.test.espresso:espresso-core:<version>'
androidTestImplementation 'androidx.test.ext:junit:<version>'

This setup allows Espresso and Compose Test APIs to work together seamlessly.

Writing Your First Espresso Test with Jetpack Compose

Let's put theory into practice and write a simple UI test. The goal here isn't to be fancy - it's to understand how Espresso and Jetpack Compose work together in a real test scenario.

We'll create a test that checks whether a button is visible on the screen and then performs a click on it.

Step 1: Create a UI test class

Start by creating a new Kotlin file inside your app's androidTest directory. You can name it something like ExampleEspressoTest.

This file will hold all your UI test logic.

Step 2: Import the required dependencies

You'll need imports from both Jetpack Compose testing and Espresso:

import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.*
import androidx.test.espresso.Espresso.*
import androidx.test.espresso.matcher.ViewMatchers.*
import org.junit.Rule
import org.junit.Test

These give you access to Compose test rules, UI matchers, and Espresso actions.

Step 3: Set up the Compose test rule

The test rule is what launches your Compose content in a controlled testing environment:

class ExampleEspressoTest {
@get:Rule
val composeTestRule = createComposeRule()
}

This rule tells the test runner how to render Compose UI before running assertions.

Step 4: Write your first test

Now for the actual test. We'll render a simple button and verify two things:

  1. The button is visible
  2. The button can be clicked
@Test
fun testButtonVisibilityAndClick() {
// Launch the Compose screen/activity
composeTestRule.setContent {
// Compose UI code here
Button(
onClick = { /* Button click action */ }
) {
Text("Click Me")
}
}

// Check if the button is displayed
onView(withText("Click Me")).check(matches(isDisplayed()))

// Perform a click action on the button
onView(withText("Click Me")).perform(click())
}

What's happening here:

  • setContent renders a Compose UI just for this test
  • Espresso verifies the button exists on screen
  • Espresso simulates a real user click

This might look simple - and that's the point. UI tests should clearly describe user behavior, not hide it behind complexity.

Step 5: Run the test

You can run the test directly from Android Studio or use the test runner to execute it as part of your test suite.

Once it passes, you've officially written and executed your first Espresso test for a Jetpack Compose UI.

From here, you can expand into testing state changes, navigation, error states, and full user flows.

Working with Matchers and Actions

Even when you're testing Jetpack Compose UI, Espresso's core ideas - matchers and actions - still apply. The difference is what you're interacting with. Instead of traditional View objects, you're now targeting Compose-based UI elements.

Matchers help Espresso find the UI element you care about, while actions define what you want to do with it - just like a real user would.

Commonly Used Matchers

Matchers are used to locate Compose components based on their properties:

  • withText("text") - Finds a composable that displays the given text.
  • isDisplayed() - Ensures the composable is currently visible on the screen.

These matchers make your tests readable and expressive, almost like describing what a user sees.

Commonly Used Actions

Actions simulate user interactions:

  • click() - Performs a tap on the matched Compose component.

When combined, matchers and actions let you write tests that read like user behavior:

"Find this button, make sure it's visible, then tap it."

This approach keeps your tests focused on what the user does, not on internal implementation details - which is exactly how good UI tests should behave.

Testing Jetpack Compose Components

When testing Compose components, you can use the onNode method to target specific components.

For example, to test a Button component:

onNode(hasText("Click Me")).performClick()

Verifying Assertions the Right Way

Assertions tell you whether your UI behaves as expected. For example:

  • isDisplayed(): Checks if the Compose component is currently visible on the screen.
  • hasText("text"): Checks if the Compose component contains the specified text.

Conclusion

Testing Jetpack Compose UI with Espresso isn't complicated - but it does require a shift in how you think about UI testing.

Compose simplifies UI structure.

Espresso ensures stability.

Assertions keep regressions in check.

Together, they help you ship UIs that behave correctly - not just in demos, but on real devices, under real conditions.

Because the best UI bug is the one your users never see.

Happy testing.

How to Use Vulkan for GPU Acceleration in Kotlin Android Apps

Published: · Last updated: · 5 min read
Robin Alex Panicker
Cofounder and CPO, Appxiom

Modern Android applications are expected to deliver smooth animations, rich visuals, and real-time graphical effects. However, executing heavy graphical operations on the CPU can quickly lead to performance bottlenecks, increased battery consumption, and poor user experience. For a broader look at tools that can elevate your Android development workflow, check out our guide on 10 Android libraries you really need.

Earlier, Android developers relied on RenderScript for GPU-accelerated workloads. With RenderScript now deprecated, Vulkan has emerged as the most powerful and future-ready alternative for high-performance graphics and compute operations on Android.

In this blog, we'll explore how to utilize GPU capabilities using Vulkan in Kotlin-based Android apps to efficiently handle intensive graphical workloads and unlock next-level performance.

Avoid Android App Crashes: Kotlin Best Practices

Published: · Last updated: · 6 min read
Andrea Sunny
Marketing Associate, Appxiom

You know that moment when you're rushing to book a cab, the payment is about to go through, and suddenly the app freezes? For a few seconds, you're stuck - did the payment go through or not? Do you retry? Do you close the app? That tiny moment of uncertainty is enough to frustrate most users. And more often than not, they don't come back.

That's exactly how silent damage begins in mobile apps. Not with big disasters-but with small, unexpected failures in moments that matter most. On Android, even one crash in a critical flow like login, checkout, or onboarding can quietly push users away, hurt your ratings, and impact revenue. While no app can ever be completely crash-proof, Kotlin gives you a strong safety net to reduce these risks long before users feel them.

Build Better, Ship Faster: 10 Android Libraries You Really Need

Published: · Last updated: · 7 min read
Sandra Rosa Antony
Software Engineer, Appxiom

Imagine building a house with your bare hands. Then, someone hands you a toolbox that automates half the work, ensures structural safety, and even paints the walls. That's what the right Android libraries feel like.

You don't just want to write code. You want to write clean, efficient, testable code that doesn't give you a migraine three months later. These 10 libraries? They're your survival kit.

Let's break them down together. I'll show you real examples, sprinkle in some numbers, and tell you exactly why each one deserves a spot in your next Android project. No fluff - just the stuff that actually helps.

How to Detect and Fix Android Memory Leaks Before They Crash Your App

Published: · Last updated: · 4 min read
Andrea Sunny
Marketing Associate, Appxiom

Have you ever dated someone who just… wouldn't let go?

You break up, move on, start fresh - and boom - they're still texting, still showing up in your life, refusing to be deleted.

That's your app with a memory leak.

It's holding on to screens, data, and objects long after it should've moved on. You've moved past the Activity, but it's still lingering in memory like a clingy ex who didn't get the memo.

The worst part? You might not even know it's happening.

But users will. They will feel it in the slowdowns, the crashes, the app that once felt smooth now feeling… emotionally unavailable.

And in Android, they're not just annoying. They're dangerous. They can slow down your app, cause freezes, and eventually - boom! A crash.

Let's dive into the most common memory leak scenarios in Android. I'll walk you through real-world examples, show you how to spot them, and most importantly, how to fix them.

How to Build an Offline-Capable Android App with Jetpack Compose and Kotlin

Published: · Last updated: · 6 min read
Robin Alex Panicker
Cofounder and CPO, Appxiom

The streak broke. So did the flow.

It wasn't that I forgot. I remembered, just a little too late.

Right before midnight, I opened the app to log my progress. But the screen just sat there, trying to connect. No internet. No log. No streak.

It sounds small, but if you've ever built a habit one day at a time, you know what a streak can mean. It's not just numbers. It's proof. And losing it? That stings.

That moment made one thing very clear: apps that help you grow should work with you, not against you, especially when the internet doesn't cooperate.

So let's build something better.

How to Avoid Memory Leaks in Jetpack Compose: Real Examples, Causes, and Fixes

Published: · Last updated: · 5 min read
Andrea Sunny
Marketing Associate, Appxiom

"Hey… why does this screen freeze every time I scroll too fast?"

That's what my QA pinged me at 11:30 AM on a perfectly normal Tuesday.

I brushed it off. "Probably a one-off," I thought.

But then the bug reports started trickling in:

  • "The app slows down after using it for a while."
  • "Navigation feels laggy."
  • "Sometimes it just… dies."

That's when the panic set in.

How to Use Gradle Flavors in Android to Build Multiple App Versions from One Codebase

Published: · Last updated: · 6 min read
Andrea Sunny
Marketing Associate, Appxiom

Ever wondered how big apps manage free vs paid versions, or white-label multiple client apps from a single Android project? The answer is Gradle Flavors.

Imagine you're building an app for a fitness startup. The client loves it. Then they say: "Can we also get a version for our premium users, with extra features and no ads? Oh, and one more version for our corporate partners?"

You smile, and quietly panic.

Do you:

  • Copy the codebase three times?
  • Manually toggle features before every build?
  • Cry?

Nope. You use Gradle Flavors.

How to Manage UX When a Bug Occurs in Your Android App

Published: · Last updated: · 5 min read
Andrea Sunny
Marketing Associate, Appxiom

You're out for dinner. Ambience? Perfect. Service? Spot on. Then your dish arrives. But something's off. The pasta's missing salt, or the steak's slightly overcooked. You raise your hand to get the waiter's attention. But before you can say a word, they smile and say, "The chef already noticed. A fresh plate is on its way."

It feels like magic. But really, it's just attention to detail before a complaint even happens.

That's the kind of experience your users expect from your app too. Silent problems fixed before they even realize something went wrong.

Building Offline-Capable Android Apps with Kotlin and Jetpack Compose

Published: · Last updated: · 5 min read
Robin Alex Panicker
Cofounder and CPO, Appxiom

In today's mobile-first world, users expect apps to work seamlessly, even when there's no internet connection. This blog post will guide you through the process of building an offline-capable Android app using Kotlin and Jetpack Compose. We'll use a ToDo app as our example to illustrate key concepts and best practices.

Architecture Overview

Before diving into the code, let's outline the architecture we'll use:

  • UI Layer: Jetpack Compose for the user interface

  • ViewModel: To manage UI-related data and business logic

  • Repository: To abstract data sources and manage data flow

  • Local Database: Room for local data persistence

  • Remote Data Source: Retrofit for API calls (when online)

  • WorkManager: For background synchronization

Setting Up the Kotlin Project

First, ensure you have the necessary dependencies in your build.gradle file:

dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.compose.ui:ui:1.4.3")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
implementation("androidx.compose.material3:material3:1.1.1")

// Room
implementation("androidx.room:room-runtime:2.5.2")
implementation("androidx.room:room-ktx:2.5.2")
kapt("androidx.room:room-compiler:2.5.2")

// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")

// WorkManager
implementation("androidx.work:work-runtime-ktx:2.8.1")
}

Implementing the Local Database

We'll use Room to store ToDo items locally. First, define the entity:

@Entity(tableName = "todos")
data class ToDo(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val title: String,
val description: String,
val isCompleted: Boolean = false,
val lastModified: Long = System.currentTimeMillis()
)

Next, create the DAO (Data Access Object):

@Dao
interface ToDoDao {
@Query("SELECT * FROM todos")
fun getAllToDos(): Flow&lt;List&lt;ToDo&gt;&gt;

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertToDo(todo: ToDo)

@Update
suspend fun updateToDo(todo: ToDo)

@Delete
suspend fun deleteToDo(todo: ToDo)
}

Finally, set up the Room database:

@Database(entities = [ToDo::class], version = 1)
abstract class ToDoDatabase : RoomDatabase() {
abstract fun todoDao(): ToDoDao

companion object {
@Volatile
private var INSTANCE: ToDoDatabase? = null

fun getDatabase(context: Context): ToDoDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java,
"todo_database"
).build()
INSTANCE = instance
instance
}
}
}
}

Implementing the Repository

The repository will manage data operations and decide whether to fetch from the local database or the remote API:

class ToDoRepository(
private val todoDao: ToDoDao,
private val apiService: ApiService
) {

val allToDos: Flow&lt;List&lt;ToDo&gt;&gt; = todoDao.getAllToDos()

suspend fun refreshToDos() {
try {
val remoteToDos = apiService.getToDos()
todoDao.insertAll(remoteToDos)
} catch (e: Exception) {
// Handle network errors
}
}

suspend fun addToDo(todo: ToDo) {
todoDao.insertToDo(todo)
try {
apiService.addToDo(todo)
} catch (e: Exception) {
// Handle network errors, maybe queue for later sync
}
}

// Implement other CRUD operations similarly
}

Setting Up the ViewModel

The ViewModel will handle the UI logic and interact with the repository:

class ToDoViewModel(private val repository: ToDoRepository) : ViewModel() {
val todos = repository.allToDos.asLiveData()

fun addToDo(title: String, description: String) {
viewModelScope.launch {
val todo = ToDo(title = title, description = description)
repository.addToDo(todo)
}
}

fun refreshToDos() {
viewModelScope.launch {
repository.refreshToDos()
}
}
// Implement other operations
}

Creating the UI with Jetpack Compose

Now, let's create the UI for our ToDo app:

@Composable
fun ToDoScreen(viewModel: ToDoViewModel) {
val todos by viewModel.todos.collectAsState(initial = emptyList())
LazyColumn {
items(todos) { todo -&gt;
ToDoItem(todo)
}
item {
AddToDoButton(viewModel)
}
}
}

@Composable
fun ToDoItem(todo: ToDo) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = todo.isCompleted,
onCheckedChange = { /* Update todo */ }
)
Column(modifier = Modifier.weight(1f)) {
Text(text = todo.title, fontWeight = FontWeight.Bold)
Text(text = todo.description)
}
}
}
}

@Composable
fun AddToDoButton(viewModel: ToDoViewModel) {
var showDialog by remember { mutableStateOf(false) }
Button(onClick = { showDialog = true }) {
Text("Add ToDo")
}
if (showDialog) {
AddToDoDialog(
onDismiss = { showDialog = false },
onConfirm = { title, description -&gt;
viewModel.addToDo(title, description)
showDialog = false
}
)
}
}

@Composable
fun AddToDoDialog(onDismiss: () -&gt; Unit, onConfirm: (String, String) -&gt; Unit) {
// Implement dialog UI here
}

Implementing Background Sync with WorkManager

To ensure our app stays up-to-date even when it's not actively running, we can use WorkManager for background synchronization:

class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
private val repository = ToDoRepository(
ToDoDatabase.getDatabase(context).todoDao(),
ApiService.create()
)
override suspend fun doWork(): Result {
return try {
repository.refreshToDos()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}

Schedule the work in your application class or main activity:

class ToDoApplication : Application() {
override fun onCreate() {
super.onCreate()
setupPeriodicSync()
}

private fun setupPeriodicSync() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val syncRequest = PeriodicWorkRequestBuilder&lt;SyncWorker&gt;(1, TimeUnit.HOURS)
.setConstraints(constraints)
.build()

WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"ToDo_Sync",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
}
}

Handling Conflicts

When working offline, conflicts may arise when syncing data. Implement a conflict resolution strategy:

suspend fun syncToDo(todo: ToDo) {
try {
val remoteToDo = apiService.getToDo(todo.id)
if (remoteToDo.lastModified &gt; todo.lastModified) {
// Remote version is newer, update local
todoDao.insertToDo(remoteToDo)
} else {
// Local version is newer, update remote
apiService.updateToDo(todo)
}
} catch (e: Exception) {
// Handle network errors
}
}

Testing Offline Functionality

To ensure your app works offline:

  • Implement a network utility class to check connectivity.

  • Use this utility in your repository to decide whether to fetch from local or remote.

  • Write unit tests for your repository and ViewModel.

  • Perform UI tests with network on and off to verify behavior.

Conclusion

Building an offline-capable Android app requires careful consideration of data flow, synchronization, and conflict resolution. By using Room for local storage, Retrofit for API calls, and WorkManager for background sync, you can create a robust offline experience for your users.

Remember to handle edge cases, such as first-time app usage without internet, and always provide clear feedback to users about the sync status of their data.

Would you like me to explain or break down any part of this code?

Gemini Nano for Android: An Offline AI Model

Published: · Last updated: · 3 min read
Don Peter
Cofounder and CTO, Appxiom

What is Gemini Nano

Gemini Nano is an efficient AI model for on device tasks according to Google. With Gemini Nano, developers will be able to deliver rich generative AI experiences without requiring a network connection or moving data off-device. Gemini Nano is ideal for use cases where low latency, low cost, and privacy safeguards are paramount.

Architecture

To handle AI services, Android OS has introduced a new module called AICore. It is available in Android 14.

AICore, a brand new system service that simplifies using AI features in your apps. AICore takes care of everything behind the scenes, from managing AI models to ensuring their safe operation on your device. AICore lets your apps run powerful AI models like Gemini Nano directly on your device. This means faster processing compared to sending data to the cloud.

Gemini Nano and AICore Architecture - Image from Google

AICore prioritizes Privacy

Limited Connections:  AICore operates in a restricted environment, only interacting with essential system functions. This isolation helps prevent unauthorized access to your data by other other apps.

  • Secure Downloads: AICore doesn't directly access the internet. Instead, it relies on a separate, privacy-focused app (Private Compute Services) to download any required models. This ensures your information stays secure during the download process.

Getting Started with Gemini Nano

You will need Google AI Edge SDK for Android to work with Gemini Nano. This SDK provides APIs for interacting with the model and managing its functionalities.

Currently the SDK is available only for private review and is not available for public, but soon it will be.

Compatibility Check

Not all Android devices support Gemini Nano. Hence you need to run a compatibility check in your code.

fun isDeviceSupported(): Boolean {
val edgeManager = AiEdgeManager.getInstance(context)
return edgeManager.isModelSupported(GeminiNano.MODEL_NAME)
}

Accessing Gemini Nano

Once you've confirmed compatibility, you can obtain an instance of the GeminiNano class using the AIEdgeManager in AI Edge SDK:

val geminiNano = AiEdgeManager.getInstance(context).getAiModel(GeminiNano.MODEL_NAME) as GeminiNano

Example: Sentiment classification

Gemini Nano claims it is good in performing inference on data. Let's run a piece of text and infer the sentiment score.

val text = "I'm happy to meet you again."
val i = TextInput.Builder().setText(text).build()
val o = geminiNano.runInference(i) as TextClassificationOutput
val sentimentClassification = o.classification

Pretty straight forward. The model classifies the sentiment and stores in a variable.

Gemini Nano UseCases

Comming back to Gemini Nano, here are some of the use cases of the model that can be put to use by developer once it is out of private beta.

  • Get smarter summaries:  Gemini Nano can condense lengthy articles or reports into easy-to-understand summaries, saving you time.

  • Find answers fast:  Have questions? Ask away! Gemini Nano can analyze text and provide relevant answers.

  • Write with confidence:  Gemini Nano helps you polish your writing with features like grammar correction, proofreading, and writing suggestions. It can even generate smart replies based on the context.

  • Analyze emotions:  Understand the sentiment behind text.  Gemini Nano can detect the overall mood or feeling expressed in writing.

  • Built-in privacy:  Use powerful AI features without compromising your data.  Gemini Nano is designed with privacy in mind.

Already on Pixel:  Several Google Pixel apps, like Voice Recorder and Gboard, leverage the power of AICore to enhance your experience.

Gradle Flavors: Building Multiple Android App Variants with Single Codebase

Published: · Last updated: · 4 min read
Don Peter
Cofounder and CTO, Appxiom

Gradle Flavors, also known as product flavors, allow developers to create multiple variants of their app within a single codebase. Each flavor can have its own unique configuration, resources, and dependencies, enabling customization based on factors such as branding, feature sets, or target audiences.

If you're developing free and paid versions of your app, adapting it for different languages or regions, or incorporating variations for testing purposes, Gradle Flavors offer unparalleled flexibility.

Why Use Gradle Flavors?

  • Customization: With Gradle Flavors, developers can easily tailor their app to suit specific user segments or market requirements. This level of customization fosters better user engagement and satisfaction.

  • Efficiency: Rather than maintaining separate codebases for different app variants, Gradle Flavors streamline the development process by centralizing code while allowing for variant-specific configurations. This results in reduced complexity and faster iteration cycles.

  • Consistency: By defining variant-specific resources and dependencies within the Gradle build script, developers ensure consistency across different app versions while minimizing the risk of errors or inconsistencies.

  • Market Segmentation: For businesses targeting diverse demographics or regions, Gradle Flavors facilitate the creation of specialized versions of the app tailored to each market segment's preferences and needs.

Free and Pro Versions

Let's illustrate the power of Gradle Flavors with the above scenario – creating free and pro versions of an app. Suppose you have an app called "WeatherApp" and want to offer both a free version with basic features and a paid pro version with additional functionalities.

android {
...
productFlavors {
free {
dimension "tier"
applicationId "com.example.weather.free"
versionCode 1
versionName "1.0"
// Define flavor-specific configurations
buildConfigField "boolean", "IS_PRO_VERSION", "false"
}
pro {
dimension "tier"
applicationId "com.example.weather.pro"
versionCode 1
versionName "1.0"
// Define flavor-specific configurations
buildConfigField "boolean", "IS_PRO_VERSION", "true"
}
}

buildTypes {
debug {
// Debug-specific configurations
...
}
release {
// Release-specific configurations
...
}
}
...
}

In this example, we define two product flavors: 'free' and 'pro', each with its own unique applicationID. We can then customize the behavior, features, and resources specific to each flavor, such as limiting certain features to the pro version or displaying different branding elements.

In the above example, we define two build types. "debug" and "release," each with its own configurations. These configurations might include signing configurations, proguard rules, or other build-specific settings.

With the flavors and build variants defined, Gradle will generate the following build variants:

  • freeDebug

  • freeRelease

  • proDebug

  • proRelease

Developers can then use these variants to build and test different versions of the app. For instance, they can build the "pro" release variant to generate a signed APK for distribution to users who have purchased the pro version of the app. Similarly, they can build the "free" debug variant to test new features or changes specific to the free version of the app.

Android Project Structure with Gradle Flavors

In the above example, the folder structure for the Android project would typically look like this:

- app
- src
- free
- java
- com
- example
- weather
- MainActivity.java
- ...
- res
- layout
- drawable
- values
- ...
- pro
- java
- com
- example
- weather
- MainActivity.java
- ...
- res
- layout
- drawable
- values
- ...
- main
- java
- com
- example
- weather
- MainActivity.java
- ...
- res
- layout
- drawable
- values
- ...

Here's a breakdown of the folder structure:

  • app: This is the main module of the Android project.

  • src: This directory contains the source code and resources for different build variants.

  • free: This directory contains the source code and resources specific to the "free" flavor.

java: Java source code files for the "free" flavor.

  • com.example.weather: Package directory.

  • MainActivity.java: Example activity class.

  • Other Java files specific to the "free" flavor.

  • res: Resource directory for the "free" flavor.

  • layout: XML layout files.

  • drawable: Image resources.

  • values: Resource files such as strings, colors, dimensions, etc.

  • Other resource directories specific to the "free" flavor.

  • pro: This directory contains the source code and resources specific to the "pro" flavor. The structure is similar to the "free" flavor but with resources and code specific to the "pro" variant.

  • main: This directory contains the main source code and resources shared among all flavors and build types. It serves as the base for all variants and contains code and resources common to both "free" and "pro" flavors.

By organizing the source code and resources in this way, Gradle can easily build different variants of the app by combining the contents of the "main" directory with the specific contents of each flavor directory (free and pro).

Leveraging Gradle flavors and build variants empowers Android developers to efficiently manage and customize multiple versions of their apps, catering to diverse user preferences and market requirements while maintaining codebase integrity and flexibility.