Skip to content

Commit

Permalink
Merge pull request #218 from rezazarchi/feature/wear
Browse files Browse the repository at this point in the history
WearOS version
  • Loading branch information
Kaaveh authored Sep 22, 2024
2 parents af3e903 + 80fe340 commit a0871be
Show file tree
Hide file tree
Showing 36 changed files with 1,069 additions and 5 deletions.
Empty file.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Check the apk [from here](asset/app_v1.0.0.apk)
The main architecture of code based on MVI + CLEAN architecture. The division criteria is a hybrid strategy based on Feature + Layer by module.
For the detail of architecture, please read [this article](https://medium.com/@kaaveh/migrate-from-mvvm-to-mvi-f938c27c214f).

## Wear OS
This project includes a WearOS module designed for Android-based smartwatches like the Galaxy Watch. The `app-watch` module contains `app`, `designsystem`, `navigation`, and `ui` submodules. You can build the `app-watch:app` to have wearOS version of the application.

## 🚦 Navigation

For the detail of navigation implementations, please read [this article](https://proandroiddev.com/all-about-navigation-in-the-jetpack-compose-based-production-code-base-902706b8466d).
Expand All @@ -34,6 +37,7 @@ For the detail of handling preview of composable functions in this code-base, pl
- Work manager
- Unit test
- Support large screens
- Support WearOS devices
- Monochromatic app icon
- Version catalog & Convention Plugin (For the detail, please read [this article](https://proandroiddev.com/mastering-android-dependency-management-b94205595f6b))
- CI
Expand Down Expand Up @@ -67,6 +71,9 @@ For the detail of handling preview of composable functions in this code-base, pl

![](asset/large_screen.jpg)

### WearOS devises (Android based smart watches)
![Wear OS screenshots](asset/wearos.jpg)

## Additional Resources

- [Git Hooks](documentation/GitHooks.md) - Learn about Git hooks used in this project for code formatting and analysis.
Expand Down
1 change: 1 addition & 0 deletions app-watch/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
64 changes: 64 additions & 0 deletions app-watch/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
plugins {
id("composenews.android.application")
id("composenews.android.application.compose")
id("composenews.android.hilt")
}

android {
namespace = libs.versions.projectApplicationId.get()
defaultConfig {
applicationId = libs.versions.projectApplicationId.get()
versionCode = libs.versions.projectVersionCode.get().toInt()
versionName = libs.versions.projectVersionName.get()
minSdk = libs.versions.projectMinSdkVersionWear.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
getByName("debug") {
isMinifyEnabled = false
isShrinkResources = false
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
}

dependencies {

projects.apply {
implementation(appWatch.navigation)
implementation(appWatch.designsystem)
}

projects.core.apply {
implementation(base)
implementation(uimarket)
}

libs.apply {
implementation(compose.activity)
implementation(androidx.ktx)
implementation(hilt.work)
implementation(lifecycle.runtime.ktx)
implementation(libs.hilt.navigation.compose)
implementation(libs.compose.ui.preview.wear)
implementation(libs.compose.foundation.wear)
implementation(libs.compose.horologist.layout)
implementation(work.runtime.ktx)
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.compose.ui.test.junit4)
debugImplementation(libs.compose.ui.tooling)
debugImplementation(libs.compose.ui.test.manifest)
}
}
8 changes: 8 additions & 0 deletions app-watch/app/lint.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Ignore the IconLocation for the Tile preview images -->
<issue id="IconLocation">
<ignore path="res/drawable/tile_preview.png" />
<ignore path="res/drawable-round/tile_preview.png" />
</issue>
</lint>
22 changes: 22 additions & 0 deletions app-watch/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn reactor.blockhound.integration.BlockHoundIntegration
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ir.composenews.ui

import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onParent
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test

class MainActivityTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

@Test
fun marketListScreen_DisplayedCorrectly() {

composeTestRule.waitUntil(timeoutMillis = 5_000) {
composeTestRule.onAllNodesWithText("BNB").fetchSemanticsNodes().isNotEmpty() ||
composeTestRule.onAllNodesWithText("BTC").fetchSemanticsNodes().isNotEmpty()
}

composeTestRule.onNodeWithText("BNB").assertExists()
composeTestRule.onNodeWithText("Bitcoin").assertExists().onParent().performClick()
composeTestRule.onNodeWithContentDescription("Loading. PLease wait").assertExists()


}
}
41 changes: 41 additions & 0 deletions app-watch/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<uses-feature android:name="android.hardware.type.watch" />

<application
android:name=".ComposeNewsWearApplication"
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<uses-library
android:name="com.google.android.wearable"
android:required="true" />

<!--
Set to true if your app is Standalone, that is, it does not require the handheld
app to run.
-->
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true" />


<activity
android:name=".ui.MainActivity"
android:exported="true"
android:taskAffinity="">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ir.composenews

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class ComposeNewsWearApplication : Application() {

override fun onCreate() {
super.onCreate()
}

}
35 changes: 35 additions & 0 deletions app-watch/app/src/main/java/ir/composenews/MainViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ir.composenews

import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import ir.composenews.appwatch.navigation.graph.MainContract
import ir.composenews.base.BaseViewModel
import ir.composenews.core_test.dispatcher.DispatcherProvider
import ir.composenews.uimarket.model.MarketModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class MainViewModel @Inject constructor(
dispatcherProvider: DispatcherProvider,
) : BaseViewModel(dispatcherProvider), MainContract {

private val mutableState = MutableStateFlow(MainContract.State())
override val state: StateFlow<MainContract.State> = mutableState.asStateFlow()
override fun event(event: MainContract.Event) {
when (event) {
is MainContract.Event.SetMarket -> setMarket(event.market)
}
}

private fun setMarket(market: MarketModel?) = viewModelScope.launch {
mutableState.emit(
mutableState.value.copy(
market
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ir.composenews.navigation

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import ir.composenews.appwatch.navigation.graph.MainContract
import ir.composenews.appwatch.navigation.graph.Destinations
import ir.composenews.appwatch.navigation.graph.wearMarketDetail
import ir.composenews.appwatch.navigation.graph.wearMarketList
import ir.composenews.uimarket.model.MarketModel

@Composable
fun ComposeNewsWearNavHost(
navController: NavHostController,
modifier: Modifier,
onMarketSelected: ((MarketModel) -> Unit)? = null,
uiState: MainContract.State
) {
SwipeDismissableNavHost(
navController = navController,
startDestination = Destinations.MarketListScreen.route,
modifier = modifier,
) {
wearMarketList(
showFavorite = false,
onMarketSelected = onMarketSelected,
)
wearMarketDetail(uiState)
}
}
61 changes: 61 additions & 0 deletions app-watch/app/src/main/java/ir/composenews/ui/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ir.composenews.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.wear.compose.material.TimeText
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import androidx.wear.tooling.preview.devices.WearDevices
import com.google.android.horologist.compose.layout.AppScaffold
import com.google.android.horologist.compose.layout.ScreenScaffold
import dagger.hilt.android.AndroidEntryPoint
import ir.composenews.MainViewModel
import ir.composenews.appwatch.designsystem.theme.ComposeNewsWearTheme
import ir.composenews.appwatch.navigation.graph.Destinations
import ir.composenews.appwatch.navigation.graph.MainContract
import ir.composenews.navigation.ComposeNewsWearNavHost

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

private val mainViewModel: MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WearApp()
}
}

@Composable
fun WearApp() {
val navController = rememberSwipeDismissableNavController()
val state = mainViewModel.state.collectAsState()
ComposeNewsWearTheme {
AppScaffold {
ScreenScaffold(timeText = { TimeText() }) {
ComposeNewsWearNavHost(
navController = navController,
onMarketSelected = {
mainViewModel.event(MainContract.Event.SetMarket(it))
navController.navigate(Destinations.MarketDetailScreen().route)
},
uiState = state.value,
modifier = Modifier
)
}
}
}
}

@Preview(device = WearDevices.LARGE_ROUND, showSystemUi = true)
@Composable
fun DefaultPreview() {
WearApp()
}
}
3 changes: 3 additions & 0 deletions app-watch/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Compose News</string>
</resources>
1 change: 1 addition & 0 deletions app-watch/designsystem/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
12 changes: 12 additions & 0 deletions app-watch/designsystem/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
id("composenews.android.library")
id("composenews.android.library.compose")
}

android {
namespace = "ir.composenews.app_watch.designsystem"
}

dependencies {
implementation(libs.compose.material.wear)
}
6 changes: 6 additions & 0 deletions app-watch/designsystem/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools">

<uses-sdk tools:overrideLibrary="androidx.wear.compose.material.core, androidx.wear.compose.foundation, androidx.wear.compose.material" />

</manifest>
Loading

0 comments on commit a0871be

Please sign in to comment.