Android Snippets

repository·main·Indexed 22 days ago

https://github.com/android/snippets

A collection of implementation snippets and sample projects designed to accompany official Android developer documentation. It includes practical examples for Bluetooth Low Energy (BLE) connectivity, UI views, Wear OS Watch Face Push patterns, AppFunction API implementations for AI agents, and Android Car App development including CarAppService, screen navigation, and MapWithContentTemplate.

Tokens
13.7K
Snippets
42
Records
45
Agent score
77%

What's inside android-snippets

  1. Access Bluetooth LE UI view snippets

    main
    This repository serves as a collection of code snippets used in the official Android developer documentation regarding UI views. You can find implementation examples for various UI components by browsing the views/ directory. These snippets are intended to be used as reference implementations for building Android user interfaces.
  2. Use Compose in an Activity

    main

    To use Jetpack Compose within a standard ComponentActivity, call setContent inside onCreate. This establishes the root of your Compose UI tree.

    class ExampleActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MaterialTheme {
                    Greeting(name = "compose")
                }
            }
        }
    }
    
    @Composable
    fun Greeting(name: String) {
        Text(text = "Hello $name!")
    }
  3. Handle transitive remember dependencies

    main

    When creating a new object that depends on an existing RememberObserver object (e.g., a Foo object that is remembered), you should use the dependency as a key in the remember function. This ensures that if the original object changes, the dependent object is also re-calculated.

    While remember { Bar(foo) } is acceptable, the recommended pattern is remember(foo) { Bar(foo) } to correctly handle updates to the dependency.

    // Assuming foo is a remembered RememberObserver
    val foo: Foo = rememberFoo()
    
    // Recommended key usage to handle updates to foo:
    val barWithKey: Bar = remember(foo) { Bar(foo) }
  4. Teardown work in onForgotten() and onAbandoned()

    main

    To prevent memory leaks and unnecessary resource usage, you must cancel work launched during the object's lifecycle using the appropriate RememberObserver callbacks:

    1. onForgotten(): Use this to cancel work launched from onRemembered(). This is called when the object is removed from the composition.
    2. onAbandoned(): Use this to cancel any work that was launched during the object's construction (the init block) if the object was never successfully remembered by the composition.

    If you are implementing a RetainObserver, use onRetired to cancel work launched from onRetained.

    class MyComposeObject : RememberObserver {
        private val job = Job()
        private val coroutineScope = CoroutineScope(Dispatchers.Main + job)
    
        override fun onForgotten() {
            // Cancel work launched from onRemembered
            job.cancel()
        }
    
        override fun onAbandoned() {
            // Cancel work launched by the constructor if the object wasn't successfully remembered
            job.cancel()
        }
    }
  5. Detect desktop engagement mode using WindowInfoTracker

    main

    To optimize your UI for desktop environments (e.g., switching between touch-optimized and pointer-optimized layouts), use WindowInfoTracker.getOrCreate(activity) to collect windowEngagementInfo. You can check for specific engagement modes, such as WindowEngagementInfo.EngagementMode.PRECISE_POINTER, to determine if the user is interacting via a precise pointing device like a mouse.

    val windowInfoTracker = WindowInfoTracker.getOrCreate(this@DesktopWindowingActivity)
    
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            windowInfoTracker.windowEngagementInfo(this@DesktopWindowingActivity)
                .collect { windowEngagementInfo ->
                    if(windowEngagementInfo.hasEngagementMode(WindowEngagementInfo.EngagementMode.PRECISE_POINTER)){
                        showDesktopOptimizedUI()
                    } else {
                        showTouchOptimizedUI()
                    }
                }
        }
    }
  6. Use Compose in a Fragment

    main

    You can integrate Compose into Fragments using several methods:

    1. Via XML: Use a ComposeView inside your fragment's XML layout. It is highly recommended to set a ViewCompositionStrategy (e.g., DisposeOnViewTreeLifecycleDestroyed) to ensure the composition is disposed of correctly when the view's lifecycle is destroyed.
    2. Via View Binding: Access the ComposeView through your binding object.
    3. Without XML: Programmatically create a ComposeView and return it from onCreateView.
    // Example: Using ComposeView in a Fragment with XML
    class ExampleFragmentXml : Fragment() {
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            val view = inflater.inflate(R.layout.fragment_example, container, false)
            val composeView = view.findViewById<ComposeView>(R.id.compose_view)
            composeView.apply {
                setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
                setContent {
                    MaterialTheme {
                        Text("Hello Compose!")
                    }
                }
            }
            return view
        }
    }
    
    // Example: Using ComposeView without XML
    class ExampleFragmentNoXml : Fragment() {
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            return ComposeView(requireContext()).apply {
                setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
                setContent {
                    MaterialTheme {
                        Text("Hello Compose!")
                    }
                }
            }
        }
    }
  7. Implement an AppFunctionService

    main

    An AppFunctionService is the entry point for exposing functions to an AI agent. To implement one:

    1. Extend AppFunctionService.
    2. Annotate the class with @AppFunctionServiceEntryPoint, providing a serviceName and the appFunctionXmlFileName.
    3. Use @AppFunction(isDescribedByKDoc = true) on suspend functions to expose them. The agent uses the KDoc to understand how to call the function.
    4. Use standard exceptions like AppFunctionInvalidArgumentException or AppFunctionElementNotFoundException to communicate failures to the agent.

    Note: This requires @RequiresApi(36).

    @RequiresApi(36)
    @AppFunctionServiceEntryPoint(
        serviceName = "TaskAppFunctionService",
        appFunctionXmlFileName = "task_app_function_service",
    )
    abstract class BaseTaskAppFunctionService : AppFunctionService() {
        @AppFunction(isDescribedByKDoc = true)
        suspend fun createTask(
            createTaskParams: CreateTaskParams,
        ): Task {
            if (createTaskParams.title == null && createTaskParams.content == null) {
                throw AppFunctionInvalidArgumentException("Title or content should be non-null")
            }
            // ... implementation
        }
    }
  8. Implement CarAppService and manage Sessions

    main

    To create a Car App, extend CarAppService and override onCreateSession. This method determines which Session to provide based on the SessionInfo.displayType. For example, you can return a different session for a DISPLAY_TYPE_CLUSTER than for the main display. You must also implement createHostValidator to define which hosts are allowed to connect to your service.

    class MyNavigationCarAppService : CarAppService() {
        override fun onCreateSession(sessionInfo: SessionInfo): Session {
            return if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) {
                ClusterSession()
            } else {
                MainDisplaySession()
            }
        }
    
        override fun createHostValidator(): HostValidator {
            return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
        }
    }
  9. Protect RememberObserver implementations via private wrappers

    main

    To prevent consumers from accessing or misusing the RememberObserver implementation directly, wrap the observer inside a remember block and return only the underlying manager or data object. This ensures the lifecycle management (initialization and teardown) remains encapsulated within your library's internal logic.

    @Composable
    fun rememberMyManager(): MyManager {
        // Protect the RememberObserver implementation by never exposing it outside the library
        return remember {
            object : RememberObserver {
                val manager = MyComposeManager()
                override fun onRemembered() = manager.initialize()
                override fun onForgotten() = manager.teardown()
                override fun onAbandoned() { /* Nothing to do if manager hasn't initialized */ }
            }
        }.manager
    }
  10. Initialize effect-driven work in onRemembered()

    main

    When implementing androidx.compose.runtime.RememberObserver, avoid launching cancellable or effect-driven work (like Coroutines) inside the class init block or during composition. This can cause work to begin prematurely. Instead, move such work into the onRemembered() callback to ensure it starts when the object is actually successfully remembered by the composition.

    class MyComposeObject : RememberObserver {
        private val job = Job()
        private val coroutineScope = CoroutineScope(Dispatchers.Main + job)
    
        // Recommended: Move any cancellable or effect-driven work into the onRemembered() callback.
        override fun onRemembered() {
            coroutineScope.launch { loadData() }
        }
    
        private suspend fun loadData() { /* ... */ }
    }