Author Archives:

Prefactoring: Clear the Way for Your New Feature

This article was adapted from a Google Tech on the Toilet (TotT) episode. You can download a printer-friendly version of this TotT episode and post it in your office.

By Rahul Singal

“First make the change easy, then make the easy change.” - paraphrased from Kent Beck 

You're working on a new feature, but the existing code wasn't written with future changes in mind. Trying to force the feature in directly gets complicated fast. One change leads to another, and before you know it you're already a few files deep fixing things you never planned to touch.

Prefactoring (short for "preparatory refactoring") is the practice of reworking existing code to make it more suitable for an upcoming change before you actually implement the new functionality. Instead of cleaning up code as an afterthought or trying to force a new feature into an incompatible structure, you restructure the codebase first.

Prefactoring helps you: 

  • Easily implement new features: Restructuring the codebase first ensures your new feature fits naturally into the code.

  • Speed up reviews: It's easier to review the refactoring and the feature in separate changes.

  • Avoid bugs: Isolating cleanups from functional logic can help prevent bugs.

  • Roll back safely: If you need to roll back, it is much easier to revert small, focused changes.

Here is a simplified example of a prefactoring change:

Change 1 (Prefactoring)

Extract display name helper to remove duplication.

Change 2 (Feature)

Add middle name support.

+ def get_display_name(user):

+    return f"{user.first_name}”  {user.last_name}


# Profile page

-    display_name = f"{user.first_name} {user.last_name}"

+    display_name = get_display_name(user)

# Email template

- greeting = f"Hi {user.first_name} {user.last_name},"

+ greeting = f"Hi {get_display_name(user)},"

    

def get_display_name(user):

-  return f"{user.first_name} {user.last_name}”

+  return f"{user.first_name} {user.middle_name} {user.last_name}"




You can prefactor a change that is already in review too! If your reviewer suggests a related cleanup during review, you can also extract it into a new base change to keep your current change focused on the feature. Note that not every cleanup needs to be prefactoring: you can do the cleanup in a follow up change if the cleanup doesn’t block your feature, or even in the same change if the cleanup is small enough.



Redesigned Google Classroom homepage with tailored views based on user’s role

Soon, Google Classroom will introduce a redesigned homepage globally across all editions to help teachers, students, and administrators easily find relevant content, resources, and tools tailored to their specific roles. The updated interface transforms the homepage into a dynamic, centralized hub that more easily surfaces existing information and tools that were previously located in different areas of Classroom. Users can still access classes in the side navigation panel and a dedicated classes module on the homepage.

The new experience, which will begin rolling out on July 27, 2026, is personalized based on a user's role and available features:

  • For teachers, a new dashboard gives actionable insights, highlights student classwork interactions, tracks assignment completion, and surfaces a feature spotlight to help discover instructional tools and resources.

  • For students, a dedicated ‘Enrolled’ view reminds learners of coursework that is due soon and helps them manage their deadlines.

  • For school leaders and IT administrators, the homepage provides a centralized view to monitor high-level performance analytics, access shortcuts for backend administrative settings, and discover relevant tools to support educators and staff.


Users who have multiple roles (such as a teacher taking a professional development class) can easily change their view dashboard by clicking into another role (for example, Teaching, Enrolled, or Admin). When a user loads the homepage, it returns to the previous role view.

To help users control their view and focus on what matters most to them, all new homepage modules are collapsible.



Please note that not all features and views will be available to all users. Eligibility is determined by the user’s role, feature access, account type, and settings.

Getting started

  • Admins: There is no admin control for the new Classroom homepage. Gemini and Gemini Notebook features will only appear if the user is in an OU with Gemini in Classroom, Gemini app, and/or Gemini Notebook enabled. Visit the Help Center to learn about managing access to Gemini in Classroom, Gemini app, Gemini Notebook, and the option to turn these services on or off for users in the Admin console.
  • End users: There is no end user setting for the new Classroom homepage. Visit the Help Center to learn more about the new Classroom homepage.

Rollout pace

Availability

  • Available to all Google Workspace customers, Workspace Individual subscribers, and users with personal Google accounts

Resources

A centralized hub for meeting resources on the new Google Meet homepage

Finding the right notes or attachments for a meeting shouldn't feel like a scavenger hunt. We’re introducing a revamped Google Meet homepage on the web to help you stay organized and prepared throughout your entire meeting workflow.

The new homepage provides a centralized view of your meeting agenda and essential artifacts, allowing you to:

  • Prepare effectively: Quickly access meeting descriptions and calendar attachments for upcoming calls. 
  • Follow through easily: Find meeting notes, recordings, and transcripts from past meetings without digging through your inbox or Drive.
  • Manage your time: Use the new week and month navigators to look ahead at your schedule or revisit previous discussions.

Getting started

  • Admins: There is no admin control for this feature. The new homepage experience will be available to all users with access to Google Meet on the web.
  • End users: The new homepage experience will be available to all Meet users on the web. Visit meet.google.com to get started.

Rollout pace

Availability

  • Available to all Google Workspace customers and Workspace Individual subscribers

Resources

Celebrating 5 years of Jetpack Compose

Posted by Rebecca Franks, Developer Relations Engineer, Nick Butcher, Product Manager, Loryn Hairston, Product Marketing Manager, Android


Today, we officially celebrate five years since the release of Jetpack Compose 1.0. From version 1.0, announced on July 28th, 2021, to our latest 1.11 release, we’ve seen the APIs evolve significantly over the years, and we’re taking a moment to celebrate.

When we officially announced the 1.0 release, we promised a simpler, faster, and more intuitive way to build native interfaces on Android. Looking back, it's safe to say that Compose didn’t just deliver on that promise, but also completely changed the Android ecosystem, with more than 68% of the top 1,000 apps using it in production today.

History

Over the last five years, Compose has grown steadily. In the early days, we explored showing you how to build layouts with the basic Box, Row, and Column. Today, we’ve expanded Compose to work not just on mobile devices, but to other form factors such as Compose for TV, WearOS, Glance for Widgets, and even display glasses with Jetpack Compose Glimmer.



We recorded an Android Developers Backstage episode with Clara Bayarri, Engineering Lead for Jetpack, and two former leads of the team, Romain Guy and Chet Haase, along with Tor Norbye, Senior Engineering Director. In this episode, they discuss the history of Compose and the early days of development.




Compose highlights over the years

Looking back

The beginnings of Compose were very different from what you know today. Two projects were happening in parallel inside the Android team.

At the time, the Views toolkit team was thinking of unbundling the UI Toolkit into a library to help with development speed, and make it easier for developers to adopt and control updates. Meanwhile, a team was working on a novel idea to build declarative layouts by embedding XML inside Kotlin, which looked something like this:



Those two efforts merged to produce what you know today - a fully declarative UI Toolkit that utilizes the power of a compiler plugin, runtime, and Kotlin:

@Composable
fun Newsfeed(stories: List<Story>) {
    LazyColumn {
        items(stories) { story ->
            Card {
                val author = story.author
                Image(painterResource(author.profilePhoto),
                    contentDescription = author.name)
                Text(author.name)
                Text(story.content)
                if (story.hasCommentsEnabled()) {
                    for(comment in story.comments) {
                        Text(comment.mainContent)
                    }
                }
            }
        }
    }
}

And you, the community, helped us very early on! Before 2021, Compose had a pre-alpha phase, which helped ensure Compose was fit to solve the problems of our developers.

One of our favorite memories is the Android Dev Challenge. We challenged the community to build four different tasks with Compose, filling our feeds with Puppy apps, clocks, and weather apps, and giving us a ton of direct feedback that helped shape the 1.0 release.

Compose has continued to evolve, from launching with a set of Material 2 components to now supporting Material 3 Expressive.

Material 2 in Compose

Material 3 Expressive in Compose

Looking ahead

As of today, Compose 1.11 is the latest version with 1.12 coming soon, offering so much more than 1.0, 5 years ago. This year, we introduced more adaptive APIs, such as FlexBox, Grid, MediaQuery, and Styles. These APIs let you advance to the next level of premium, adaptive UI development with Compose.

At Google I/O 2026, we announced that we are now Compose-first, meaning that all future UI development will happen only in Compose, while the Views toolkit enters maintenance mode. Material Design is also shifting focus entirely to Compose, signaling an end to the findViewById era.

Community is at the heart of Compose 

Over the years, you’ve inspired us with creative examples of how you’ve used Compose, and we’d love to highlight a few more examples of where we’ve seen exciting work. Jetbrains has been a great partner for Google with Compose, expanding Compose to work across platforms with Compose Multiplatform and enabling desktop, iOS, and web developers to also enjoy the benefits of Compose.

We’ve really enjoyed following our most beloved newsletters from JetpackCompose.app’s Dispatch, AndroidWeekly, to jetc - helping Android Developers stay up-to-date with the latest in the world of Compose and Android.

Another standout contributor is sinasamaki. They’ve created many delightful experiences using Compose, such as this fun ribbon modifier and the glitchy effect:


Saket Narayan has also always been an inspiration when it comes to creating useful tools for Compose, such as telephoto, a library featuring support for pan and zoom gestures and automatic sub-sampling of large images, or the latest library, Touch Robot, which allows you to easily test interaction animations:

paparazzi.gif(end = 3_000) {
  DebitCard(
    Modifier.testTag("card")
  )

  val touchRobot = rememberTouchRobot()
  LaunchedEffect(Unit) {
    touchRobot.onNode(hasTestTag("card")).performGesture {
      draw(
        path = createAndroidHeadPath(),
        duration = 3.seconds,
      )
    }
  }
}

/** A path drawing the Android head. */
fun createAndroidHeadPath(bounds: Rect): Path = TODO()

Jake Wharton, who has used Compose in innovative ways (like molecule, and even building UI with Compose for the terminal with mosaic). Chris Banes, who has built many Compose libraries over the years, with our most recent favourite - Haze for background blurring, and many of the Android Google Developer Experts like Akshay Chordiya, Huyen Tue Dao, and Katie Barnett, who’ve contributed to the success of Compose. But this is not about selecting individuals - there have been so many great contributors to the Compose codebase, and many of you continue to inspire us with your fun examples, libraries, and in-depth talks. Without the community, Jetpack Compose wouldn’t be as successful as it is today.

Cheers to the next 5 years, and more! 

Jetpack Compose has grown from an experimental idea into the standard for Android UI Development. Thank you to the entire Toolkit team at Google, and to the incredible global developer community that wrote libraries, filed bugs, and pushed the boundaries of what declarative UI can do.

This week, we’ll be celebrating with some in-person birthday parties across the globe, and a live “Birthday party” on the Android Developers YouTube channel on July 30th at 13:00 UTC. During this time, we’ll hang out and discuss Compose and answer your questions!

Cheers to the next 5 years, and happy composing!

Build intelligent Android apps: Cloud and hybrid inference

Posted by Thomas Ezan, Jolanda Verhoef, Caren Chang, Senior Developer Relations Engineers, Android Developer Relations



Welcome back to the blog post series "Build intelligent Android apps" where we take a basic Android app and transform it into a personalized, intelligent, and agentic experience. In our previous post we explored how to build intelligent on-device features using Gemini Nano through ML Kit's Prompt API.

In this post, we will look at how you can leverage Firebase AI Logic to build cloud-hosted and hybrid AI features: 

  • Grounding answers in real-world context
  • Routing requests dynamically between cloud and local execution using hybrid inference
  • Translating content with custom routing systems


Sometimes a use case requires AI models with greater world knowledge, a much larger context window, or the ability to handle complex queries. In those scenarios, we can leverage cloud models. 

Other times, you want the best of both worlds: using hybrid inference to run on-device when available to lower costs, while falling back to the cloud to ensure compatibility for all devices.


Cloud and hybrid features in Jetpacker: Museum assistant with web grounding, hybrid restaurant review drafting, and  support chat featuring custom-routed live translation.

Let’s look at how we implemented three cloud and hybrid features in Jetpacker:

  • a museum assistant with web grounding
  • hybrid restaurant review drafting
  • hotel support chat featuring custom-routed live translation.

Use LLM grounding for up-to-date informationMuseum assistant chatbot with LLM grounding

The Museum assistant is an interactive chatbot designed to help users plan their museum visits. It provides visitors with up-to-date details regarding specific exhibits, current opening hours, ticket pricing, and more.


Museum assistant is a chatbot that answers questions, such as 
‘How can I get a ticket discount for Le Louvre?’

When building AI features, getting the model to answer with fresh, accurate, and specific real-world information is a common challenge. While cloud models possess massive amounts of world knowledge, they might not know about seasonal exhibits or the current day’s opening hours. 



Grounding data is added to the context window to enable the model
 to answer questions correctly and accurately.

To bridge this gap, we can use grounding techniques to add extra context to the model’s context window. The Firebase AI Logic SDK supports three types of grounding:

  • URL grounding: Grounding responses using content from a specific webpage (e.g. current ticket prices or museum rules).
  • Google Search grounding: Letting the model query the real-time Google search index for up-to-date details.
  • Maps grounding: Using Google Maps location data.

In Jetpacker, we dynamically construct the available tools based on enabled feature flags and initialize the generative model using the Firebase AI SDK:

// implementation("com.google.firebase:firebase-ai-logic")

private var toolList = mutableListOf<Tool>()

init {
    if (ENABLE_SEARCH_GROUNDING) {
        toolList.add(Tool.googleSearch())
    }
    if (ENABLE_URL_GROUNDING) {
        toolList.add(Tool.urlContext())
    }
}

private val generativeModel = Firebase.ai(backend = GenerativeBackend.googleAI())
    .generativeModel(
        modelName = "gemini-3-flash",
        systemInstruction = content {
            text("You are a helpful museum assistant answering questions about a museum. Use plain text.")
        },
        tools = toolList
    )

When the user queries the assistant, if URL grounding is enabled, we append the specific museum resource URLs directly into the prompt:

val groundingText = if (FeatureFlags.ENABLE_URL_GROUNDING) {
    "\n If the following message above is about the rules and terms to visit Le Louvre, " +
    "if needed answer this urls ${urlList.joinToString()}"
} else {
    ""
}

val prompt = "$text $groundingText"

var response = chat.sendMessage(prompt)

Hybrid inference: On-device review generation with Maps deep link

Not every AI task requires a cloud-based model, and not every device is online. To help developers balance latency, cost, and offline availability, we recently introduced the Firebase API for Hybrid Inference.

In Jetpacker, the restaurant review feature lets users review select topics and automatically drafts a review. To enable this for all users, we prioritize local execution with Gemini Nano, and fall back to cloud models on devices that don’t support Gemini Nano. 


The restaurant review feature uses hybrid inference to draft a review based on topics

// implementation("com.google.firebase:firebase-ai-logic")
// implementation("com.google.firebase:firebase-ai-ondevice:16.0.0-beta03")


// Initialize the model with hybrid routing configuration
val reviewModel = Firebase.ai.generativeModel(
    modelName = "gemini-3.1-flash-lite",
    onDeviceConfig = OnDeviceConfig(
        inferenceMode = InferenceMode.PREFER_ON_DEVICE
    )
)

The Hybrid Inference API supports four distinct routing modes:

  • PREFER_ON_DEVICE: Prioritizes local execution and falls back to cloud if Gemini Nano is unavailable.
  • PREFER_IN_CLOUD: Prioritizes cloud execution and falls back to on-device if the device goes offline.
  • ONLY_ON_DEVICE: Restricts execution strictly to the device.
  • ONLY_IN_CLOUD: Restricts execution strictly to the cloud.

Once the review is generated, we copy it to the clipboard and use an intent to open Google Maps directly to the restaurant's review page, providing a seamless user experience:

private fun copyAndOpenMapsReview(context: Context, reviewText: String, placeId: String) {
    val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    val clip = ClipData.newPlainText("User Review", reviewText)
    clipboard.setPrimaryClip(clip)

    val uri = Uri.parse("https://search.google.com/local/writereview/mobile?placeid=$placeId")
    val intent = Intent(Intent.ACTION_VIEW, uri).apply {
        setPackage("com.google.android.apps.maps")
    }
    context.startActivity(intent)
}

Custom hybrid routing: Hotel support chat translation with simulated personas

The hotel support chat was built to let users finalize logistics and check on hotel details. This feature uses system instructions to configure a localized receptionist assistant. By passing specific information—such as the preferred language and hotel information—in the instructions, we can set up a conversational persona representing a specific hotel.

private val generativeModel = Firebase.ai(backend = GenerativeBackend.googleAI())
    .generativeModel(
        systemInstruction = content {
            text("""
              You are a helpful hotel receptionist at $hotelName only speaking $language. 
              Answer politely in $language. The bar closes at 10pm and breakfast is from 7am to 10am.
              There's someone at the desk 24/7. You can retrieve your luggage from the storage room 
              at the back of the lobby at any time.
              """)
        },
        modelName = "gemini-3-flash-preview"
    )

Because receptionist responses are in the hotel's local language (for example, French for Hotel Le Meurice in Paris), we need to translate messages to the user’s preferred language. 


Hotel support chat messages are automatically translated to the user’s preferred language 

While hybrid models can configure simple routing preferences, complex scenarios require custom routing logic. In Jetpacker, we implement a custom routing stack that takes into account:

  • Language identification: Using the on-device ML Kit Language Identification API, we can detect the incoming message language.
  • On-device translation (Gemini Nano): ML Kit’s Prompt API lets us translate common language pairs directly on the device, saving bandwidth and cloud cost.
  • Cloud translation (Gemini 3 Flash): For more complex languages, we use Gemini Flash 3 to get a higher quality translation.
// implementation("com.google.android.gms:play-services-mlkit-language-id:17.0.0") 

// ML Kit for Language Identification (powered by Google Play Services)
private val languageIdentifier = LanguageIdentification.getClient()

// On-device translator model (prefer Gemini Nano) for translating common language pairs
private val hybridTranslationModel = Firebase.ai(backend = GenerativeBackend.googleAI())
    .generativeModel(
        modelName = "gemini-3-flash",
        onDeviceConfig = OnDeviceConfig(mode = InferenceMode.PREFER_ON_DEVICE)
    )

// Cloud translator model for more complex language pairs
private val cloudTranslationModel = Firebase.ai(backend = GenerativeBackend.googleAI())
    .generativeModel(
        modelName = "gemini-3-flash"
    )

When a message needs to be translated, we identify the source language and apply our custom routing logic, executing either on-device or cloud translation:

fun translateMessage(message: SupportChatMessage) {
    viewModelScope.launch {
        // 1. Detect language using ML Kit Language Identification
        val sourceLang = try {
            Tasks.await(languageIdentifier.identifyLanguage(message.text))
        } catch (e: Exception) {
            "Undefined"
        }

        // 2. Custom routing: we've verified the translation quality for English and Korean with Gemini Nano, and will translate message on-device for those two languages
        val routeToCloud = sourceLang != "en" && sourceLang != "kr"

        val prompt = "Translate the following text to $selectedLanguage. Just return the translated sentence: ${message.text}."

        val (translatedText, routePrefix) = if (routeToCloud) {
            val result = cloudTranslationModel.generateContent(prompt)
            result.text to "[Cloud]"
        } else {
            val result = hybridTranslationModel.generateContent(prompt)
            result.text to "[On-Device]"
        }

        if (translatedText != null) {
            _translations.update { current ->
                current + (message.id to "$routePrefix: $translatedText")
            }
        }
    }
}

In this example, the custom routing logic only takes into consideration the translation’s source and target language. However, based on your app’s use case, you can expand the routing logic to include other factors such as the on-device model version, network connectivity, battery status, and more.

Securing the AI Pipelines: Firebase App Check

Lastly, using AI in the cloud opens up possibilities of API key abuse or unauthorized billing. To secure API calls, we integrated Firebase App Check using both Play Integrity (production) and the local Debug Provider (for local development or emulators).

In the JetPackerApplication.kt file, we install the debug provider at startup and trigger anonymous authentication to establish a secure user session:

//  implementation("com.google.firebase:firebase-appcheck-playintegrity") 
//  implementation("com.google.firebase:firebase-appcheck-debug")  
//  implementation("com.google.firebase:firebase-auth") 

override fun onCreate() {
    super.onCreate()
    Firebase.initialize(context = this)
    Firebase.appCheck.installAppCheckProviderFactory(
        DebugAppCheckProviderFactory.getInstance()
    )
    Firebase.auth.signInAnonymously()
}

When building locally on an emulator, App Check prints a local token secret to logcat:

Enter this debug secret into the allow list in the Firebase Console: a8c2dd4c-xxxx-xxxx-xxxx-ef6c114ba27e

Once registered in the Firebase console, local requests are fully verified and authenticated by App Check, protecting our backend while letting us test the app locally.

Conclusion

By combining cloud model capabilities (grounding, system instructions) with on-device capabilities (hybrid routing, translation, security app checks), we created a travel app that is smart, secure, and available offline.

Check out the full source code for Jetpacker on GitHub, and explore the Firebase documentation to get started:

Firebase AI Logic Documentation
Firebase Hybrid Inference API

Learn more

Check out the other parts of this blog post series:

Part 1: Introduction of the app and a high-level overview.
Part 2: On-device intelligence. Deep-dive into ML Kit’s GenAI APIs and Gemini Nano to build privacy-first features like itinerary summarization, receipt parsing, and local audio processing.
Part 3 (this post!): Hybrid and cloud reasoning. Explore how to use Firebase AI Logic to ground LLM answers in real-world data like Google Maps and web context.
Part 4: System integration. Integrating with the Android intelligence system using AppFunctions. 
Part 5 (coming soon): In-app agentic workflows. Extend the app with an end-to-end booking assistant powered by A2UI and ADK.

Interested in more on Android Development? Follow Android Developers on YouTube or LinkedIn!

All code snippets in this blog post follow the following copyright notice:

Copyright 2026 Google LLC.
SPDX-License-Identifier: Apache-2.0