diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..08ac532
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,44 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+insert_final_newline = true
+max_line_length = 145
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.gradle]
+ij_continuation_indent_size = 8
+
+[{*.kt, *.kts}]
+ij_continuation_indent_size = 8
+ij_kotlin_assignment_wrap = normal
+ij_kotlin_call_parameters_new_line_after_left_paren = true
+ij_kotlin_call_parameters_right_paren_on_new_line = true
+ij_kotlin_call_parameters_wrap = on_every_item
+ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
+ij_kotlin_continuation_indent_for_chained_calls = false
+ij_kotlin_continuation_indent_for_expression_bodies = false
+ij_kotlin_continuation_indent_in_argument_lists = false
+ij_kotlin_continuation_indent_in_elvis = false
+ij_kotlin_continuation_indent_in_if_conditions = false
+ij_kotlin_continuation_indent_in_parameter_lists = false
+ij_kotlin_continuation_indent_in_supertype_lists = false
+ij_kotlin_extends_list_wrap = normal
+ij_kotlin_if_rparen_on_new_line = true
+ij_kotlin_keep_blank_lines_before_right_brace = 0
+ij_kotlin_keep_blank_lines_in_code = 1
+ij_kotlin_keep_blank_lines_in_declarations = 1
+ij_kotlin_method_call_chain_wrap = normal
+ij_kotlin_method_parameters_new_line_after_left_paren = true
+ij_kotlin_method_parameters_right_paren_on_new_line = true
+ij_kotlin_method_parameters_wrap = on_every_item
+ij_kotlin_name_count_to_use_star_import = 2147483647
+ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
+ij_kotlin_wrap_expression_body_functions = 1
+ij_kotlin_imports_layout = *
+
+[*.md]
+trim_trailing_whitespace = false
+max_line_length = 80
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 0000000..a2e4ad7
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,147 @@
+name: Android CI
+
+on:
+ push:
+ branches:
+ - main
+ - orbit/main
+ - feature/**
+ tags:
+ - '**'
+ pull_request:
+ branches:
+ - main
+ - orbit/main
+ - feature/**
+
+jobs:
+ static-checks:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+
+ - name: gradle cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: gradle wrapper cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.gradle/wrapper/dists
+ key: ${{ runner.os }}-gradlewrapper
+
+ - name: Detekt
+ run: ./gradlew detekt
+
+ - name: Markdown lint
+ run: ./gradlew markdownlint
+
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+
+ - name: gradle cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: gradle wrapper cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.gradle/wrapper/dists
+ key: ${{ runner.os }}-gradlewrapper
+
+ - name: Lint
+ run: ./gradlew lint
+
+ unit-tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+
+ - name: gradle cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: gradle wrapper cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.gradle/wrapper/dists
+ key: ${{ runner.os }}-gradlewrapper
+
+ - name: konan cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.konan
+ key: ${{ runner.os }}-konan
+
+ - name: Unit tests
+ run: ./gradlew check -xlint
+
+ - name: Upload test artifacts
+ if: failure()
+ uses: actions/upload-artifact@master
+ with:
+ name: test-results
+ path: '**/build/reports/tests/**'
+
+ build:
+ needs: [ static-checks, lint, unit-tests ]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+
+ - name: gradle cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: gradle wrapper cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.gradle/wrapper/dists
+ key: ${{ runner.os }}-gradlewrapper
+
+ - name: konan cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.konan
+ key: ${{ runner.os }}-konan
+
+ - name: Build
+ run: ./gradlew clean assemble -xassembleDebug
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
new file mode 100644
index 0000000..634bc49
--- /dev/null
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -0,0 +1,23 @@
+name: "Validate Gradle Wrapper"
+
+on:
+ push:
+ branches:
+ - main
+ - orbit/main
+ - feature/**
+ tags:
+ - '*'
+ pull_request:
+ branches:
+ - main
+ - orbit/main
+ - feature/**
+
+jobs:
+ validation:
+ name: "Validation"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: gradle/wrapper-validation-action@v1
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..31d06eb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,275 @@
+# Created by https://www.gitignore.io/api/linux,macos,kotlin,windows,android,androidstudio
+# Edit at https://www.gitignore.io/?templates=linux,macos,kotlin,windows,android,androidstudio
+
+### Android ###
+# Built application files
+*.apk
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+#*.jks
+#*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
+
+### Android Patch ###
+gen-external-apklibs
+output.json
+
+# Replacement of .externalNativeBuild directories introduced
+# with Android Studio 3.5.
+.cxx/
+
+### Kotlin ###
+# Compiled class file
+
+# Log file
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+### AndroidStudio ###
+# Covers files to be ignored for android development using Android Studio.
+
+# Built application files
+
+# Files for the ART/Dalvik VM
+
+# Java class files
+
+# Generated files
+
+# Gradle files
+.gradle
+
+# Signing files
+.signing/
+
+# Local configuration file (sdk path, etc)
+
+# Proguard folder generated by Eclipse
+
+# Log Files
+
+# Android Studio
+/*/build/
+/*/local.properties
+/*/out
+/*/*/build
+/*/*/production
+*.ipr
+*.swp
+
+# Android Patch
+
+# External native build folder generated in Android Studio 2.2 and later
+
+# NDK
+obj/
+
+# IntelliJ IDEA
+*.iws
+/out/
+
+# User-specific configurations
+.idea/caches/
+.idea/libraries/
+.idea/shelf/
+.idea/.name
+.idea/compiler.xml
+.idea/copyright/profiles_settings.xml
+.idea/encodings.xml
+.idea/misc.xml
+.idea/scopes/scope_settings.xml
+.idea/vcs.xml
+.idea/jsLibraryMappings.xml
+.idea/datasources.xml
+.idea/dataSources.ids
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+
+# OS-specific files
+.DS_Store?
+
+# Legacy Eclipse project files
+.classpath
+.project
+.cproject
+.settings/
+
+# Mobile Tools for Java (J2ME)
+
+# Package Files #
+
+# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
+
+## Plugin-specific files:
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Mongo Explorer plugin
+.idea/mongoSettings.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+### AndroidStudio Patch ###
+
+!/gradle/wrapper/gradle-wrapper.jar
+
+# End of https://www.gitignore.io/api/linux,macos,kotlin,windows,android,androidstudio
diff --git a/NOTICE.md b/NOTICE.md
new file mode 100644
index 0000000..b7f509e
--- /dev/null
+++ b/NOTICE.md
@@ -0,0 +1,13 @@
+# NOTICES
+
+## Orbit MVI
+
+Copyright © 2021 Mikołaj Leszczyński & Appmattus Limited.
+
+This software uses the following open source works:
+
+- [Kotlin](https://github.com/JetBrains/kotlin)
+- [Android](https://developer.android.com)
+- [Orbit MVI](https://github.com/orbit-mvi/orbit-mvi)
+- [Lightstreamer](https://lightstreamer.com)
+- [Dagger](https://dagger.dev)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4ff5838
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+# Orbit Sample - Compose Stock List
+
+This sample implements a stock list using [Orbit MVI](https://github.com/orbit-mvi/orbit-mvi).
+
+- The application uses Dagger Hilt for dependency injection which is initialised
+ in [StockListApplication](app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/StockListApplication.kt).
+
+- Streaming data is provided by [Lightstreamer](https://lightstreamer.com) and
+ their demo server with callback interfaces converted to Kotlin Flow's with
+ [callbackFlow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/callback-flow.html).
+
+- Navigation between the stock list and the detail view uses Jetpack's [Navigation](https://developer.android.com/jetpack/compose/navigation).
+ [ListViewModel](app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListViewModel.kt)
+ posts a side effect which [ListScreen](app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/ui/ListScreen.kt)
+ observes and sends to the `NavController`.
+
+- [Jetpack Compose](https://developer.android.com/jetpack/compose)
+ is used to render layouts throughout.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..76bb9a9
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.application")
+ kotlin("android")
+ id("kotlin-parcelize")
+ id("androidx.navigation.safeargs.kotlin")
+ id("dagger.hilt.android.plugin")
+ kotlin("kapt")
+}
+
+android {
+ compileSdk = 30
+ defaultConfig {
+ minSdk = 23
+ targetSdk = 30
+ versionCode = 1
+ versionName = "1.0"
+ applicationId = "org.orbitmvi.orbit.sample.stocklist.compose"
+ }
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ }
+ }
+ compileOptions {
+ isCoreLibraryDesugaringEnabled = true
+
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.0.0"
+ }
+
+ buildFeatures {
+ // Enables Jetpack Compose for this module
+ compose = true
+ }
+
+ testOptions.unitTests.isIncludeAndroidResources = true
+
+ packagingOptions {
+ resources {
+ excludes.addAll(
+ listOf(
+ "META-INF/INDEX.LIST",
+ "META-INF/io.netty.versions.properties"
+ )
+ )
+ }
+ }
+
+ sourceSets {
+ get("main").java.srcDir("src/main/kotlin")
+ get("test").java.srcDir("src/test/kotlin")
+ }
+}
+
+dependencies {
+ implementation(kotlin("stdlib-jdk8"))
+ implementation("org.orbit-mvi:orbit-core:4.1.3")
+ implementation("org.orbit-mvi:orbit-viewmodel:4.1.3")
+
+ implementation("androidx.navigation:navigation-fragment-ktx:2.3.5")
+ implementation("androidx.navigation:navigation-ui-ktx:2.3.5")
+ implementation("com.lightstreamer:ls-android-client:4.2.5")
+ implementation("androidx.lifecycle:lifecycle-common-java8:2.4.0-alpha02")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha02")
+
+ // Dependency Injection
+ implementation("com.google.dagger:hilt-android:2.38.1")
+ kapt("com.google.dagger:hilt-android-compiler:2.38.1")
+
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
+
+ // Jetpack Compose
+ implementation("androidx.activity:activity-compose:1.3.0")
+ implementation("androidx.compose.ui:ui:1.0.0")
+ // Tooling support (Previews, etc.)
+ implementation("androidx.compose.ui:ui-tooling:1.0.0")
+ // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
+ implementation("androidx.compose.foundation:foundation:1.0.0")
+ // Material Design
+ implementation("androidx.compose.material:material:1.0.0")
+ // Material design icons
+ implementation("androidx.compose.material:material-icons-core:1.0.0")
+ // Navigation
+ implementation("androidx.navigation:navigation-compose:2.4.0-alpha05")
+ implementation("androidx.hilt:hilt-navigation-compose:1.0.0-alpha03")
+ // Lifecycle
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07")
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..61114b4
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..b11d7bd
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/MainActivity.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/MainActivity.kt
new file mode 100644
index 0000000..30e4b0c
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/MainActivity.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+import org.orbitmvi.orbit.sample.stocklist.detail.business.DetailViewModel
+import org.orbitmvi.orbit.sample.stocklist.detail.ui.DetailScreen
+import org.orbitmvi.orbit.sample.stocklist.list.business.ListViewModel
+import org.orbitmvi.orbit.sample.stocklist.list.ui.ListScreen
+import org.orbitmvi.orbit.sample.stocklist.streaming.StreamingClient
+
+@AndroidEntryPoint
+class MainActivity : AppCompatActivity() {
+
+ @Inject
+ lateinit var streamingClient: StreamingClient
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ lifecycle.addObserver(streamingClient)
+
+ setContent {
+ val navController = rememberNavController()
+ NavHost(navController, startDestination = "list") {
+ composable("list") {
+ val viewModel = hiltViewModel()
+ ListScreen(navController, viewModel)
+ }
+ composable("detail/{itemName}") {
+ val viewModel = hiltViewModel()
+ DetailScreen(navController, viewModel)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/StockListApplication.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/StockListApplication.kt
new file mode 100644
index 0000000..970c7e1
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/StockListApplication.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@Suppress("unused")
+@HiltAndroidApp
+class StockListApplication : Application()
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/common/ui/AppBar.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/common/ui/AppBar.kt
new file mode 100644
index 0000000..2a4306d
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/common/ui/AppBar.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.common.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import org.orbitmvi.orbit.sample.stocklist.R
+
+@Composable
+fun AppBar(topAppBarText: String, onBackPressed: (() -> Unit)? = null) {
+ TopAppBar(
+ title = {
+ Row {
+ Image(
+ painterResource(id = R.drawable.ic_orbit_toolbar),
+ contentDescription = null
+ )
+ Text(
+ text = topAppBarText
+ )
+ }
+ },
+ navigationIcon = onBackPressed?.let {
+ {
+ IconButton(onClick = onBackPressed) {
+ Icon(
+ Icons.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ }
+ },
+ backgroundColor = Color.White,
+ contentColor = Color.Black
+ )
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/common/ui/PriceBox.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/common/ui/PriceBox.kt
new file mode 100644
index 0000000..1e9d44e
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/common/ui/PriceBox.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.common.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import org.orbitmvi.orbit.sample.stocklist.R
+import org.orbitmvi.orbit.sample.stocklist.streaming.stock.Tick
+
+@Composable
+fun PriceBox(
+ price: String,
+ priceTick: Tick?,
+ color: Color,
+ style: TextStyle,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ shape = RectangleShape,
+ elevation = 4.dp,
+ color = color,
+ modifier = modifier
+ .padding(vertical = 4.dp)
+ .padding(end = 4.dp)
+ ) {
+ Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
+ priceTick?.let {
+ Icon(
+ painter = painterResource(id = if (priceTick == Tick.Up) R.drawable.arrow_up_bold else R.drawable.arrow_down_bold),
+ contentDescription = null,
+ tint = Color.White,
+ )
+ } ?: Spacer(Modifier)
+
+ Text(
+ price,
+ style = style,
+ color = Color.White,
+ textAlign = TextAlign.End,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(4.dp)
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PriceBoxPreview() {
+ Column {
+ PriceBox("2.64", Tick.Up, colorResource(android.R.color.holo_red_dark), MaterialTheme.typography.body2)
+
+ PriceBox("2.63", Tick.Down, colorResource(android.R.color.holo_blue_dark), MaterialTheme.typography.body2)
+
+ PriceBox("2.61", null, colorResource(android.R.color.holo_red_dark), MaterialTheme.typography.body2)
+ }
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/common/ui/StockItem.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/common/ui/StockItem.kt
new file mode 100644
index 0000000..5bc85d7
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/common/ui/StockItem.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.list.ui
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import org.orbitmvi.orbit.sample.stocklist.common.ui.PriceBox
+import org.orbitmvi.orbit.sample.stocklist.streaming.stock.Stock
+import org.orbitmvi.orbit.sample.stocklist.streaming.stock.Tick
+
+@Composable
+fun StockItem(stock: Stock, onClick: (stock: Stock) -> Unit) {
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onClick(stock) }
+ .padding(8.dp)) {
+ Text(
+ stock.name,
+ style = MaterialTheme.typography.subtitle1,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Row(horizontalArrangement = Arrangement.SpaceEvenly) {
+
+ PriceBox(
+ price = stock.bid,
+ priceTick = stock.bidTick,
+ color = colorResource(android.R.color.holo_red_dark),
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .weight(1f)
+ .align(Alignment.CenterVertically)
+ )
+ PriceBox(
+ price = stock.ask,
+ priceTick = stock.askTick,
+ color = colorResource(android.R.color.holo_blue_dark),
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .weight(1f)
+ )
+
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .padding(vertical = 4.dp)
+ .padding(end = 4.dp),
+ contentAlignment = Alignment.CenterEnd
+ ) {
+ Text(
+ stock.timestamp,
+ style = MaterialTheme.typography.body2,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier
+ .padding(4.dp)
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun StocksPreview() {
+ val stock = Stock("1", "Anduct", "2.64", Tick.Up, "2.65", Tick.Down, "06:35:08")
+ StockItem(stock) {}
+}
+
+@Preview
+@Composable
+fun StocksPreview3() {
+ val stock = Stock("1", "Anduct", "2.64", Tick.Down, "2.65", Tick.Up, "06:35:08")
+ StockItem(stock) {}
+}
+
+@Preview
+@Composable
+fun StocksPreview2() {
+ val stock = Stock("1", "Anduct", "2.64", null, "2.65", null, "06:35:08")
+ StockItem(stock) {}
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/detail/business/DetailState.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/detail/business/DetailState.kt
new file mode 100644
index 0000000..f00036a
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/detail/business/DetailState.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.detail.business
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import org.orbitmvi.orbit.sample.stocklist.streaming.stock.StockDetail
+
+@Parcelize
+data class DetailState(
+ val stock: StockDetail? = null
+) : Parcelable
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/detail/business/DetailViewModel.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/detail/business/DetailViewModel.kt
new file mode 100644
index 0000000..d2cf6aa
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/detail/business/DetailViewModel.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.detail.business
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.collect
+import org.orbitmvi.orbit.ContainerHost
+import org.orbitmvi.orbit.sample.stocklist.streaming.stock.StockRepository
+import org.orbitmvi.orbit.syntax.simple.intent
+import org.orbitmvi.orbit.syntax.simple.reduce
+import org.orbitmvi.orbit.viewmodel.container
+
+@HiltViewModel
+class DetailViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val stockRepository: StockRepository
+) : ViewModel(), ContainerHost {
+
+ private val itemName = savedStateHandle.get("itemName")!!
+
+ override val container = container(DetailState(), savedStateHandle) { requestStock() }
+
+ private fun requestStock(): Unit = intent(registerIdling = false) {
+ stockRepository.stockDetails(itemName).collect {
+ reduce {
+ state.copy(stock = it)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/detail/ui/DetailScreen.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/detail/ui/DetailScreen.kt
new file mode 100644
index 0000000..da16d0f
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/detail/ui/DetailScreen.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.detail.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import org.orbitmvi.orbit.sample.stocklist.R
+import org.orbitmvi.orbit.sample.stocklist.common.ui.AppBar
+import org.orbitmvi.orbit.sample.stocklist.common.ui.PriceBox
+import org.orbitmvi.orbit.sample.stocklist.detail.business.DetailViewModel
+
+@Composable
+@Suppress("LongMethod")
+fun DetailScreen(navController: NavController, viewModel: DetailViewModel) {
+
+ val state = viewModel.container.stateFlow.collectAsState().value
+
+ Column {
+ AppBar(state.stock?.name ?: stringResource(id = R.string.app_name)) {
+ navController.popBackStack()
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+
+ PriceBox(
+ price = state.stock?.bid ?: "",
+ priceTick = state.stock?.bidTick,
+ color = colorResource(android.R.color.holo_red_dark),
+ style = MaterialTheme.typography.body1,
+ modifier = Modifier
+ .weight(1f)
+ .height(48.dp)
+ )
+ PriceBox(
+ price = state.stock?.ask ?: "",
+ priceTick = state.stock?.askTick,
+ color = colorResource(android.R.color.holo_blue_dark),
+ style = MaterialTheme.typography.body1,
+ modifier = Modifier
+ .weight(1f)
+ .height(48.dp)
+ )
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ modifier = Modifier
+ ) {
+ Text(
+ text = state.stock?.bidQuantity ?: "",
+ textAlign = TextAlign.End,
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ Text(
+ text = state.stock?.askQuantity ?: "",
+ textAlign = TextAlign.End,
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ modifier = Modifier.padding(top = 16.dp)
+ ) {
+ Text(
+ text = "Change %:",
+ textAlign = TextAlign.End,
+ style = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ Text(
+ text = state.stock?.pctChange ?: "",
+ textAlign = TextAlign.Start,
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ }
+ Row(
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ modifier = Modifier.padding(top = 8.dp)
+ ) {
+ Text(
+ text = "Timestamp:",
+ textAlign = TextAlign.End,
+ style = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ Text(
+ text = state.stock?.timestamp ?: "",
+ textAlign = TextAlign.Start,
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ }
+ Row(
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ modifier = Modifier.padding(top = 8.dp)
+ ) {
+ Text(
+ text = "High:",
+ textAlign = TextAlign.End,
+ style = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ Text(
+ text = state.stock?.max ?: "",
+ textAlign = TextAlign.Start,
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ }
+ Row(
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ modifier = Modifier.padding(top = 8.dp)
+ ) {
+ Text(
+ text = "Low:",
+ textAlign = TextAlign.End,
+ style = MaterialTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ Text(
+ text = state.stock?.min ?: "",
+ textAlign = TextAlign.Start,
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/di/AppModule.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/di/AppModule.kt
new file mode 100644
index 0000000..5b805c3
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/di/AppModule.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+import org.orbitmvi.orbit.sample.stocklist.streaming.StreamingClient
+import org.orbitmvi.orbit.sample.stocklist.streaming.stock.StockRepository
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AppModule {
+ @Provides
+ @Singleton
+ fun provideStreamingClient(): StreamingClient = StreamingClient()
+
+ @Provides
+ @Singleton
+ fun provideStockRepository(streamingClient: StreamingClient): StockRepository = StockRepository(streamingClient)
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListSideEffect.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListSideEffect.kt
new file mode 100644
index 0000000..5f695e5
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListSideEffect.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.list.business
+
+sealed class ListSideEffect {
+ data class NavigateToDetail(val itemName: String) : ListSideEffect()
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListState.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListState.kt
new file mode 100644
index 0000000..415cb7d
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListState.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.list.business
+
+import android.os.Parcelable
+import org.orbitmvi.orbit.sample.stocklist.streaming.stock.Stock
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class ListState(
+ val stocks: List = emptyList()
+) : Parcelable
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListViewModel.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListViewModel.kt
new file mode 100644
index 0000000..179c2f5
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/business/ListViewModel.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.list.business
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.collect
+import org.orbitmvi.orbit.ContainerHost
+import org.orbitmvi.orbit.sample.stocklist.streaming.stock.StockRepository
+import org.orbitmvi.orbit.syntax.simple.intent
+import org.orbitmvi.orbit.syntax.simple.postSideEffect
+import org.orbitmvi.orbit.syntax.simple.reduce
+import org.orbitmvi.orbit.viewmodel.container
+
+@HiltViewModel
+class ListViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val stockRepository: StockRepository
+) : ViewModel(), ContainerHost {
+
+ override val container = container(ListState(), savedStateHandle) { requestStocks() }
+
+ private fun requestStocks(): Unit = intent(registerIdling = false) {
+ stockRepository.stockList().collect {
+ reduce {
+ state.copy(stocks = it)
+ }
+ }
+ }
+
+ fun viewMarket(itemName: String) = intent {
+ postSideEffect(ListSideEffect.NavigateToDetail(itemName))
+ }
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/ui/ListScreen.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/ui/ListScreen.kt
new file mode 100644
index 0000000..ee916cb
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/list/ui/ListScreen.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.list.ui
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavController
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import org.orbitmvi.orbit.sample.stocklist.R
+import org.orbitmvi.orbit.sample.stocklist.common.ui.AppBar
+import org.orbitmvi.orbit.sample.stocklist.list.business.ListSideEffect
+import org.orbitmvi.orbit.sample.stocklist.list.business.ListViewModel
+
+@Composable
+fun ListScreen(navController: NavController, viewModel: ListViewModel) {
+
+ val state = viewModel.container.stateFlow.collectAsState().value
+
+ LaunchedEffect(viewModel) {
+ launch {
+ viewModel.container.sideEffectFlow.collect { handleSideEffect(navController, it) }
+ }
+ }
+
+ Column {
+ AppBar(stringResource(id = R.string.app_name))
+
+ LazyColumn {
+ items(state.stocks) { stock ->
+ StockItem(stock) {
+ viewModel.viewMarket(stock.itemName)
+ }
+ }
+ }
+ }
+}
+
+private fun handleSideEffect(navController: NavController, sideEffect: ListSideEffect) {
+ when (sideEffect) {
+ is ListSideEffect.NavigateToDetail -> navController.navigate("detail/${sideEffect.itemName}")
+ }
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/EmptySubscriptionListener.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/EmptySubscriptionListener.kt
new file mode 100644
index 0000000..e36f401
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/EmptySubscriptionListener.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) Lightstreamer Srl
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.streaming
+
+import com.lightstreamer.client.ItemUpdate
+import com.lightstreamer.client.Subscription
+import com.lightstreamer.client.SubscriptionListener
+
+/**
+ * Empty SubscriptionListener
+ */
+@Suppress("TooManyFunctions")
+object EmptySubscriptionListener : SubscriptionListener {
+ override fun onListenEnd(p0: Subscription) = Unit
+
+ override fun onItemUpdate(p0: ItemUpdate) = Unit
+
+ override fun onSubscription() = Unit
+
+ override fun onEndOfSnapshot(p0: String?, p1: Int) = Unit
+
+ override fun onItemLostUpdates(p0: String?, p1: Int, p2: Int) = Unit
+
+ override fun onSubscriptionError(p0: Int, p1: String?) = Unit
+
+ override fun onClearSnapshot(p0: String?, p1: Int) = Unit
+
+ override fun onCommandSecondLevelSubscriptionError(p0: Int, p1: String?, p2: String?) = Unit
+
+ override fun onUnsubscription() = Unit
+
+ override fun onCommandSecondLevelItemLostUpdates(p0: Int, p1: String) = Unit
+
+ override fun onListenStart(p0: Subscription) = Unit
+
+ override fun onRealMaxFrequency(p0: String?) = Unit
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/StreamingClient.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/StreamingClient.kt
new file mode 100644
index 0000000..6312b18
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/StreamingClient.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.streaming
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.lightstreamer.client.LightstreamerClient
+import com.lightstreamer.client.Subscription
+import kotlin.concurrent.thread
+
+class StreamingClient : DefaultLifecycleObserver {
+ private var connectionWish = false
+
+ private val lsClient = LightstreamerClient(
+ "http://push.lightstreamer.com:80",
+ "DEMO"
+ ).apply {
+ connect()
+ }
+
+ override fun onStart(owner: LifecycleOwner) {
+ synchronized(lsClient) {
+ connectionWish = true
+ lsClient.connect()
+ }
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ synchronized(lsClient) {
+ connectionWish = false
+
+ thread {
+ @Suppress("MagicNumber")
+ Thread.sleep(5000)
+ synchronized(lsClient) {
+ if (!connectionWish) {
+ lsClient.disconnect()
+ }
+ }
+ }
+ }
+ }
+
+ fun addSubscription(sub: Subscription) {
+ lsClient.subscribe(sub)
+ }
+
+ fun removeSubscription(sub: Subscription) {
+ lsClient.unsubscribe(sub)
+ }
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/Stock.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/Stock.kt
new file mode 100644
index 0000000..c209973
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/Stock.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.streaming.stock
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class Stock(
+ val itemName: String,
+ val name: String,
+ val bid: String,
+ val bidTick: Tick?,
+ val ask: String,
+ val askTick: Tick?,
+ val timestamp: String
+) : Parcelable
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/StockDetail.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/StockDetail.kt
new file mode 100644
index 0000000..a82da78
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/StockDetail.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.streaming.stock
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class StockDetail(
+ val itemName: String,
+ val name: String,
+ val pctChange: String,
+ val bid: String,
+ val bidTick: Tick?,
+ val bidQuantity: String,
+ val ask: String,
+ val askTick: Tick?,
+ val askQuantity: String,
+ val min: String,
+ val max: String,
+ val timestamp: String
+) : Parcelable
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/StockRepository.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/StockRepository.kt
new file mode 100644
index 0000000..59ff4a4
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/StockRepository.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.streaming.stock
+
+import android.text.format.DateUtils
+import com.lightstreamer.client.ItemUpdate
+import com.lightstreamer.client.Subscription
+import com.lightstreamer.client.SubscriptionListener
+import java.math.RoundingMode
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import org.orbitmvi.orbit.sample.stocklist.streaming.EmptySubscriptionListener
+import org.orbitmvi.orbit.sample.stocklist.streaming.StreamingClient
+
+@Suppress("MagicNumber", "ComplexCondition")
+class StockRepository(private val client: StreamingClient) {
+
+ private val items = (1..20).map { "item$it" }.toTypedArray()
+ private val subscriptionFields = arrayOf("stock_name", "bid", "ask", "timestamp")
+ private val detailSubscriptionFields =
+ arrayOf("stock_name", "timestamp", "pct_change", "bid_quantity", "bid", "ask", "ask_quantity", "min", "max")
+
+ @Suppress("EXPERIMENTAL_API_USAGE")
+ fun stockList(): Flow> = callbackFlow {
+ val stockList = MutableList(20) { null }
+
+ val bidJobs = mutableMapOf()
+ val askJobs = mutableMapOf()
+
+ trySend(emptyList())
+
+ val subscription = Subscription("MERGE", items, subscriptionFields).apply {
+ dataAdapter = "QUOTE_ADAPTER"
+ requestedMaxFrequency = "1"
+ requestedSnapshot = "yes"
+ addListener(
+ object : SubscriptionListener by EmptySubscriptionListener {
+ override fun onItemUpdate(p0: ItemUpdate) {
+ val itemName = p0.itemName
+ val stockName = p0.getValue("stock_name")
+ val formattedBid = p0.getValue("bid")?.to2dp()
+ val formattedAsk = p0.getValue("ask")?.to2dp()
+ val formattedTimestamp = p0.getValue("timestamp")?.toFormattedTimestamp()
+
+ if (itemName != null && stockName != null && formattedBid != null && formattedAsk != null &&
+ formattedTimestamp != null
+ ) {
+ val bidTick = tickDirection(stockList[p0.itemPos - 1]?.bid, formattedBid)
+ val askTick = tickDirection(stockList[p0.itemPos - 1]?.ask, formattedAsk)
+
+ stockList[p0.itemPos - 1] = stockList[p0.itemPos - 1]?.copy(
+ bid = formattedBid,
+ ask = formattedAsk,
+ timestamp = formattedTimestamp
+ ) ?: Stock(itemName, stockName, formattedBid, null, formattedAsk, null, formattedTimestamp)
+
+ bidTick?.let {
+ bidJobs[p0.itemPos]?.cancel()
+
+ stockList[p0.itemPos - 1] = stockList[p0.itemPos - 1]?.copy(bidTick = bidTick)
+
+ bidJobs[p0.itemPos] = async {
+ @Suppress("MagicNumber")
+ (delay(300))
+
+ stockList[p0.itemPos - 1] = stockList[p0.itemPos - 1]?.copy(bidTick = null)
+ trySend(stockList.filterNotNull())
+ }
+ }
+
+ askTick?.let {
+ askJobs[p0.itemPos]?.cancel()
+
+ stockList[p0.itemPos - 1] = stockList[p0.itemPos - 1]?.copy(askTick = askTick)
+
+ askJobs[p0.itemPos] = async {
+ @Suppress("MagicNumber")
+ (delay(300))
+
+ stockList[p0.itemPos - 1] = stockList[p0.itemPos - 1]?.copy(askTick = null)
+ trySend(stockList.filterNotNull())
+ }
+ }
+
+ trySend(stockList.filterNotNull())
+ }
+ }
+ }
+ )
+ }
+
+ client.addSubscription(subscription)
+
+ awaitClose {
+ client.removeSubscription(subscription)
+ }
+ }
+
+ private fun tickDirection(currentValue: String?, newValue: String): Tick? {
+ if (newValue != currentValue && !currentValue.isNullOrEmpty()) {
+ val diff = newValue.toDouble().compareTo(currentValue.toDouble())
+
+ if (diff != 0) {
+ return if (diff > 0) Tick.Up else Tick.Down
+ }
+ }
+ return null
+ }
+
+ @Suppress("EXPERIMENTAL_API_USAGE")
+ fun stockDetails(itemName: String): Flow = callbackFlow {
+ var detail: StockDetail? = null
+ var bidJob: Job? = null
+ var askJob: Job? = null
+
+ val subscription = Subscription("MERGE", itemName, detailSubscriptionFields).apply {
+ dataAdapter = "QUOTE_ADAPTER"
+ requestedSnapshot = "yes"
+
+ addListener(
+ object : SubscriptionListener by EmptySubscriptionListener {
+ @Suppress("LongMethod")
+ override fun onItemUpdate(p0: ItemUpdate) {
+ val stockName = p0.getValue("stock_name")
+ val pctChange = p0.getValue("pct_change")?.to2dp()
+ val formattedBid = p0.getValue("bid")?.to2dp()
+ val bidQuantity = p0.getValue("bid_quantity")
+ val formattedAsk = p0.getValue("ask")?.to2dp()
+ val askQuantity = p0.getValue("ask_quantity")
+ val min = p0.getValue("min")?.to2dp()
+ val max = p0.getValue("max")?.to2dp()
+ val formattedTimestamp = p0.getValue("timestamp")?.toFormattedTimestamp()
+
+ if (stockName != null &&
+ pctChange != null &&
+ formattedBid != null &&
+ bidQuantity != null &&
+ formattedAsk != null &&
+ askQuantity != null &&
+ min != null &&
+ max != null &&
+ formattedTimestamp != null
+ ) {
+ val bidTick = tickDirection(detail?.bid, formattedBid)
+ val askTick = tickDirection(detail?.ask, formattedAsk)
+
+ detail = detail?.copy(
+ pctChange = pctChange,
+ bid = formattedBid,
+ bidQuantity = bidQuantity,
+ ask = formattedAsk,
+ askQuantity = askQuantity,
+ min = min,
+ max = max,
+ timestamp = formattedTimestamp
+ ) ?: StockDetail(
+ itemName = itemName,
+ name = stockName,
+ pctChange = pctChange,
+ bid = formattedBid,
+ bidTick = null,
+ bidQuantity = bidQuantity,
+ ask = formattedAsk,
+ askTick = null,
+ askQuantity = askQuantity,
+ min = min,
+ max = max,
+ timestamp = formattedTimestamp
+ )
+
+ bidTick?.let {
+ bidJob?.cancel()
+
+ detail = detail?.copy(bidTick = bidTick)
+
+ bidJob = async {
+ @Suppress("MagicNumber")
+ (delay(300))
+
+ detail = detail?.copy(bidTick = null)
+ trySend(detail!!)
+ }
+ }
+
+ askTick?.let {
+ askJob?.cancel()
+
+ detail = detail?.copy(askTick = askTick)
+
+ askJob = async {
+ @Suppress("MagicNumber")
+ (delay(300))
+
+ detail = detail?.copy(askTick = null)
+ trySend(detail!!)
+ }
+ }
+
+ trySend(detail!!)
+ }
+ }
+ }
+ )
+ }
+
+ client.addSubscription(subscription)
+
+ awaitClose {
+ client.removeSubscription(subscription)
+ }
+ }
+
+ fun String.to2dp(): String = toBigDecimal().setScale(2, RoundingMode.HALF_UP).toPlainString()
+
+ fun String.toFormattedTimestamp(): String = toLong().let { rawTimestamp ->
+ if (DateUtils.isToday(rawTimestamp)) {
+ DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM)
+ LocalDateTime.ofInstant(Instant.ofEpochMilli(rawTimestamp), ZoneId.systemDefault())
+ .format(DateTimeFormatter.ofPattern("HH:mm:ss"))
+ } else {
+ LocalDateTime.ofInstant(Instant.ofEpochMilli(rawTimestamp), ZoneId.systemDefault())
+ .format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/Tick.kt b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/Tick.kt
new file mode 100644
index 0000000..40f00a0
--- /dev/null
+++ b/app/src/main/kotlin/org/orbitmvi/orbit/sample/stocklist/streaming/stock/Tick.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.orbitmvi.orbit.sample.stocklist.streaming.stock
+
+enum class Tick {
+ Up, Down
+}
diff --git a/app/src/main/res/drawable/arrow_down_bold.xml b/app/src/main/res/drawable/arrow_down_bold.xml
new file mode 100644
index 0000000..e751aa0
--- /dev/null
+++ b/app/src/main/res/drawable/arrow_down_bold.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/arrow_up_bold.xml b/app/src/main/res/drawable/arrow_up_bold.xml
new file mode 100644
index 0000000..607561d
--- /dev/null
+++ b/app/src/main/res/drawable/arrow_up_bold.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..b4afc77
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_orbit_toolbar.xml b/app/src/main/res/drawable/ic_orbit_toolbar.xml
new file mode 100644
index 0000000..c760523
--- /dev/null
+++ b/app/src/main/res/drawable/ic_orbit_toolbar.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..bc0eef2
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..bc0eef2
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..b868402
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,19 @@
+
+
+
+ #000000
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8e2fa83
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,19 @@
+
+
+
+ Orbit Compose Stock List
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d271e6c
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..861d605
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import com.appmattus.markdown.rules.LineLengthRule
+import com.appmattus.markdown.rules.ProperNamesRule
+import com.appmattus.markdown.rules.ProperNamesRule.Companion.DefaultNames
+import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath("com.android.tools.build:gradle:7.0.0")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10")
+ classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5")
+ classpath("com.google.dagger:hilt-android-gradle-plugin:2.38.1")
+ }
+}
+
+plugins {
+ id("com.github.ben-manes.versions") version "0.39.0"
+ id("com.appmattus.markdown") version "0.6.0"
+}
+
+apply(from = "gradle/scripts/detekt.gradle.kts")
+
+tasks.register("clean") {
+ delete(rootProject.buildDir)
+}
+
+tasks.withType {
+ resolutionStrategy {
+ componentSelection {
+ all {
+ fun isNonStable(version: String) = listOf(
+ "alpha",
+ "beta",
+ "rc",
+ "cr",
+ "m",
+ "preview",
+ "b",
+ "ea"
+ ).any { qualifier ->
+ version.matches(Regex("(?i).*[.-]$qualifier[.\\d-+]*"))
+ }
+ if (isNonStable(candidate.version) && !isNonStable(currentVersion)) {
+ reject("Release candidate")
+ }
+ }
+ }
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven { setUrl("https://www.lightstreamer.com/repo/maven") }
+ maven { setUrl("https://dl.bintray.com/lisawray/maven") }
+ }
+}
+
+markdownlint {
+ rules {
+ +LineLengthRule(codeBlocks = false, tables = false)
+ +ProperNamesRule(names = DefaultNames + "Orbit")
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..4a5436b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,34 @@
+#
+# Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/gradle/scripts/detekt.gradle.kts b/gradle/scripts/detekt.gradle.kts
new file mode 100644
index 0000000..1601bf6
--- /dev/null
+++ b/gradle/scripts/detekt.gradle.kts
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 Babylon Partners Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import io.gitlab.arturbosch.detekt.Detekt
+import io.gitlab.arturbosch.detekt.DetektPlugin
+
+buildscript {
+ repositories {
+ maven(url = "https://plugins.gradle.org/m2/")
+ }
+ dependencies {
+ classpath("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.17.1")
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+apply()
+
+tasks.named("detekt", Detekt::class.java).configure {
+ setSource(files(rootProject.projectDir))
+
+ include("**/*.kt")
+ include("**/*.kts")
+ exclude("**/resources/**")
+ exclude("**/build/**")
+
+ parallel = true
+
+ autoCorrect = true
+ buildUponDefaultConfig = true
+ config.setFrom(files("${rootProject.projectDir}/gradle/scripts/detekt.yml"))
+
+ reports {
+ xml {
+ enabled = true
+ destination = file("build/reports/detekt/detekt.xml")
+ }
+ html {
+ enabled = true
+ }
+ }
+}
+
+dependencies {
+ "detektPlugins"("io.gitlab.arturbosch.detekt:detekt-formatting:1.17.1")
+}
diff --git a/gradle/scripts/detekt.yml b/gradle/scripts/detekt.yml
new file mode 100644
index 0000000..bdf51cb
--- /dev/null
+++ b/gradle/scripts/detekt.yml
@@ -0,0 +1,16 @@
+build:
+ maxIssues: 0
+
+formatting:
+ MaximumLineLength:
+ active: false
+
+style:
+ MaxLineLength:
+ maxLineLength: 145 # increased from 120
+ MagicNumber:
+ excludes: "**/test/**,**/androidTest/**,**Test.kt,**Spec.kt,**Spek.kt,**.kts"
+
+complexity:
+ LongMethod:
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..29e4134
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/images/icon.svg b/images/icon.svg
new file mode 100644
index 0000000..147bb91
--- /dev/null
+++ b/images/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..b3d7ab1
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 Mikołaj Leszczyński & Appmattus Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+include("app")