Skip to content

Commit

Permalink
Add Jetpack Glance sample (#20)
Browse files Browse the repository at this point in the history
* Create glance sample app

* Add hello world, action handling, error ui, stateful and list widgets

* Add comments to the logic for reading the installed widgets info

* Add widgets size mode examples

* Center the content in the size mode examples

* Add remote views interop examples

* Update widgets metadata  + Add docs for the hello world and action widgets

* Add docs for remainder of widgets

* Update widgets preview images

* Update dependencies

* Add screenshots

* Create README.md
  • Loading branch information
husaynhakeem authored Jan 17, 2022
1 parent 29bd367 commit 7b84af4
Show file tree
Hide file tree
Showing 68 changed files with 1,803 additions and 0 deletions.
10 changes: 10 additions & 0 deletions GlanceSample/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
14 changes: 14 additions & 0 deletions GlanceSample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Glance - App widgets

Android sample app to learn about the Jetpack Glance library, a recently introduced addition to Jetpack, built on top of compose that makes building app widgets easier.

<img src="https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/screenshot/widgets-glance.png" alt="glance_widgets" height="1500">

The app showcases:
- Building a basic app widget ([HelloWorldWidget](https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/app/src/main/java/com/husaynhakeem/glancesample/widget/HelloWorldWidget.kt))
- Handling user interactions by firing a callback, or launching an Activity, Service or BroadcastReceiver ([ActionWidget](https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/app/src/main/java/com/husaynhakeem/glancesample/widget/action/ActionWidget.kt)).
- Handling widget errors and providing a custom error UI ([ErrorUIWidget](https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/app/src/main/java/com/husaynhakeem/glancesample/widget/ErrorUIWidget.kt)).
- Composing the widget's UI using the UI components Glance offers, and using GlanceModifier to decorate them and define their behavior ([ListWidget](https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/app/src/main/java/com/husaynhakeem/glancesample/widget/ListWidget.kt)).
- Building stateful app widgets, i.e storing the state of the widget's UI using a data store (e.g Preferences) and saving to it, as well as updating both the widget's state and refreshing its UI when the state changes ([StatefulWidget](https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/app/src/main/java/com/husaynhakeem/glancesample/widget/StatefulWidget.kt)).
- Choosing the right size mode to define how the widget's UI reacts to the user resizing it. Glance provides 3 options to choose from: Single, Exact and Responsive ([SizeSingleWidget](https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/app/src/main/java/com/husaynhakeem/glancesample/widget/size/SizeSingleWidget.kt), [SizeExactWidget](https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/app/src/main/java/com/husaynhakeem/glancesample/widget/size/SizeExactWidget.kt), [SizeResponsiveWidget](https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/app/src/main/java/com/husaynhakeem/glancesample/widget/size/SizeResponsiveWidget.kt)).
- Using Glance in interoperability with RemoteViews, the traditional way of building app widgets ([RemoteViewInteropWidget](https://github.com/husaynhakeem/android-playground/blob/master/GlanceSample/app/src/main/java/com/husaynhakeem/glancesample/widget/interop/RemoteViewInteropWidget.kt)).
1 change: 1 addition & 0 deletions GlanceSample/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
40 changes: 40 additions & 0 deletions GlanceSample/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}

android {
compileSdkVersion 31
defaultConfig {
applicationId "com.husaynhakeem.glancesample"
minSdkVersion 21
targetSdkVersion 31
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
useIR = true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion kotlin_version
}
}

dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
implementation 'androidx.activity:activity-compose:1.4.0'
implementation "androidx.glance:glance-appwidget:1.0.0-alpha01"
}
172 changes: 172 additions & 0 deletions GlanceSample/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.husaynhakeem.glancesample">

<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.GlanceSample">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GlanceSample.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

<activity android:name=".widget.action.DummyActivity" />

<service android:name=".widget.action.DummyService" />

<receiver android:name=".widget.action.DummyBroadcastReceiver" />

<!--Glance app widgets receivers-->
<receiver
android:name=".widget.HelloWorldWidgetReceiver"
android:enabled="true"
android:exported="false">

<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_hello_world_info" />

</receiver>

<receiver
android:name=".widget.action.ActionWidgetReceiver"
android:enabled="true"
android:exported="false">

<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_action_info" />

</receiver>

<receiver
android:name=".widget.ListWidgetReceiver"
android:enabled="true"
android:exported="false">

<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_list_info" />

</receiver>

<receiver
android:name=".widget.ErrorUIWidgetReceiver"
android:enabled="true"
android:exported="false">

<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_error_ui_info" />

</receiver>

<receiver
android:name=".widget.StatefulWidgetReceiver"
android:enabled="true"
android:exported="false">

<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_stateful_info" />

</receiver>

<!--Widget.size-->
<receiver
android:name=".widget.size.SizeExactWidgetReceiver"
android:enabled="true"
android:exported="false">

<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_size_exact_info" />

</receiver>

<receiver
android:name=".widget.size.SizeResponsiveWidgetReceiver"
android:enabled="true"
android:exported="false">

<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_size_responsive_info" />

</receiver>

<receiver
android:name=".widget.size.SizeSingleWidgetReceiver"
android:enabled="true"
android:exported="false">

<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_size_single_info" />

</receiver>

<!--Interop with remote views-->
<receiver
android:name=".widget.interop.RemoteViewWidgetProvider"
android:enabled="true"
android:exported="false" />

<receiver
android:name=".widget.interop.RemoteViewInteropWidgetReceiver"
android:enabled="true"
android:exported="false">

<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_interop_info" />

</receiver>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.husaynhakeem.glancesample

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.lifecycle.lifecycleScope
import com.husaynhakeem.glancesample.ui.theme.GlanceSampleTheme
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

class MainActivity : AppCompatActivity() {

private val viewModel by viewModels<MainViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel
.widgets
.onEach { updateWidgets(it) }
.launchIn(lifecycleScope)
}

private fun updateWidgets(widgets: List<Widget>) {
setContent {
GlanceSampleTheme {
Surface(color = MaterialTheme.colors.background) {
WidgetInfo(widgets = widgets)
}
}
}
}
}

@Composable
fun WidgetInfo(widgets: List<Widget>) {
LazyColumn {
items(widgets) { widget ->
Text(text = widget.className)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.husaynhakeem.glancesample

import android.app.Application
import android.appwidget.AppWidgetManager
import android.content.Context
import androidx.compose.ui.unit.DpSize
import androidx.glance.GlanceId
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class MainViewModel(application: Application) : AndroidViewModel(application) {

private val _widgets = MutableStateFlow<List<Widget>>(emptyList())
val widgets = _widgets.asStateFlow()

init {
viewModelScope.launch {
_widgets.emit(getAppWidgets(application))
}
}

@Suppress("ConvertCallChainIntoSequence")
private suspend fun getAppWidgets(context: Context): List<Widget> {
val manager = GlanceAppWidgetManager(context)
return AppWidgetManager.getInstance(context)
// Get all the installed widget providers
.installedProviders
// Filter only this app's widgets providers
.filter { widgetProviderInfo -> widgetProviderInfo.provider.packageName == context.packageName }
// Get this app's widget provider classes
.mapNotNull { widgetProviderInfo -> Class.forName(widgetProviderInfo.provider.className) }
// Filter only glance widget providers
.filter { GlanceAppWidgetReceiver::class.java.isAssignableFrom(it) }
// Get each widget provider's widget
.map { (it.newInstance() as GlanceAppWidgetReceiver).glanceAppWidget.javaClass }
// Convert each widget to a UI model
.map {
val metadata = manager.getGlanceIds(it)
.map { id ->
WidgetMetadata(
id = id,
sizes = manager.getAppWidgetSizes(id),
)
}
Widget(
className = it.simpleName,
metadata = metadata,
)
}
}
}

data class Widget(
val className: String,
val metadata: List<WidgetMetadata>
)

data class WidgetMetadata(
val id: GlanceId,
val sizes: List<DpSize>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.husaynhakeem.glancesample.ui.theme

import androidx.compose.ui.graphics.Color

val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.husaynhakeem.glancesample.ui.theme

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp

val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
Loading

0 comments on commit 7b84af4

Please sign in to comment.