├── .editorconfig ├── .gitignore ├── ParallaxColumn ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── nick │ │ └── mirosh │ │ └── parallaxcolumn │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── nick │ │ └── mirosh │ │ └── parallaxcolumn │ │ ├── ImageUtils.kt │ │ └── ParallaxColumn.kt │ └── test │ └── java │ └── nick │ └── mirosh │ └── parallaxcolumn │ └── ExampleUnitTest.kt ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ ├── java │ │ └── SideEffectsScreen.kt │ └── res │ │ └── raw │ │ ├── amine_msiouri.jpg │ │ ├── connor_danylenko.jpg │ │ ├── felix.jpg │ │ ├── julia_volk.jpg │ │ ├── lukas_dlutko.jpg │ │ ├── matthew_montrone.jpg │ │ ├── pixabay.jpg │ │ └── sam_willis.jpg │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── nick │ │ │ └── mirosh │ │ │ └── androidsamples │ │ │ ├── MainActivity.kt │ │ │ ├── PokemonApi.kt │ │ │ ├── UrlUtils.kt │ │ │ ├── database │ │ │ ├── AppDatabase.kt │ │ │ └── ArticleDao.kt │ │ │ ├── di │ │ │ ├── CoroutinesModule.kt │ │ │ ├── DatabaseModule.kt │ │ │ ├── RepositoryModule.kt │ │ │ └── RetrofitModule.kt │ │ │ ├── dto │ │ │ └── PokemonResultDto.kt │ │ │ ├── models │ │ │ ├── DatabaseTodo.kt │ │ │ ├── Pokemon.kt │ │ │ └── Todo.kt │ │ │ ├── networking │ │ │ └── HeaderInterceptor.kt │ │ │ ├── repository │ │ │ ├── PokemonRepository.kt │ │ │ ├── PokemonRepositoryImpl.kt │ │ │ ├── TodoRepository.kt │ │ │ └── TodoRepositoryImpl.kt │ │ │ ├── responses │ │ │ └── PokemonResponse.kt │ │ │ └── ui │ │ │ ├── MyApplication.kt │ │ │ ├── RecompositionDestinations.kt │ │ │ ├── android │ │ │ └── AndroidApisScreen.kt │ │ │ ├── background_processing │ │ │ └── multiple_processes │ │ │ │ ├── MySameProcessService.kt │ │ │ │ ├── MySeparateProcessService.kt │ │ │ │ └── ProcessesScreen.kt │ │ │ ├── coroutines │ │ │ ├── async │ │ │ │ ├── AsyncComparisonScreen.kt │ │ │ │ └── AsyncComparisonViewModel.kt │ │ │ ├── cooperative_coroutine │ │ │ │ ├── CooperativeCancellationScreen.kt │ │ │ │ └── CooperativeCancellationViewModel.kt │ │ │ ├── coroutine_scope │ │ │ │ ├── CoroutineScopeScreen.kt │ │ │ │ └── CoroutineScopeViewModel.kt │ │ │ ├── deadlock │ │ │ │ ├── DeadLockScreen.kt │ │ │ │ └── DeadLockViewModel.kt │ │ │ ├── exceptions │ │ │ │ ├── different_exceptions │ │ │ │ │ ├── DifferentExceptionsScreen.kt │ │ │ │ │ └── DifferentExceptionsViewModel.kt │ │ │ │ ├── exception_propagation │ │ │ │ │ ├── ExceptionPropagationScreen.kt │ │ │ │ │ └── ExceptionPropagationViewModel.kt │ │ │ │ └── lobby │ │ │ │ │ └── CoroutineExceptionsLobbyScreen.kt │ │ │ ├── lobby │ │ │ │ └── CoroutinesLobbyScreen.kt │ │ │ ├── remember_coroutine_scope │ │ │ │ └── RememberCoroutineScopeScreen.kt │ │ │ ├── shared_ui_elements │ │ │ │ ├── ProgressBar.kt │ │ │ │ └── ProgressBarWithCancel.kt │ │ │ ├── synchronization │ │ │ │ └── RaceConditionViewModel.kt │ │ │ └── utils.kt │ │ │ ├── dialog │ │ │ └── WebViewDialog.kt │ │ │ ├── jetpack_compose │ │ │ ├── animation │ │ │ │ └── AnimationContent.kt │ │ │ ├── bottom_nav │ │ │ │ ├── BottomBarScreen.kt │ │ │ │ ├── BottomNavGraph.kt │ │ │ │ ├── BottomNavigationScreen.kt │ │ │ │ ├── FavoritesScreen.kt │ │ │ │ └── HomeScreen.kt │ │ │ ├── drag_drop_modifier │ │ │ │ └── DragDropModifierScreen.kt │ │ │ ├── lobby │ │ │ │ └── ComposeLobbyScreen.kt │ │ │ ├── measuring │ │ │ │ └── MeasuringComposable.kt │ │ │ ├── parallax │ │ │ │ ├── CollapsibleHeaderScreen.kt │ │ │ │ └── ParallaxColumnRunner.kt │ │ │ ├── produce_state │ │ │ │ ├── ProduceStateScreen.kt │ │ │ │ └── UserProfileViewModel.kt │ │ │ ├── progress │ │ │ │ ├── .editorconfig │ │ │ │ ├── AnimatedCounter.kt │ │ │ │ ├── ProgressBarContent.kt │ │ │ │ └── ProgressBarViewModel.kt │ │ │ ├── search │ │ │ │ ├── SearchScreen.kt │ │ │ │ └── SearchScreenViewModel.kt │ │ │ └── side_effects │ │ │ │ ├── LaunchedEffectScreen.kt │ │ │ │ ├── SideEffectsViewModel.kt │ │ │ │ └── disposable_effect │ │ │ │ └── DisposableEffectScreen.kt │ │ │ ├── main │ │ │ ├── MainScreenContent.kt │ │ │ ├── MainViewModel.kt │ │ │ └── SimpleListScreenContent.kt │ │ │ ├── navigation │ │ │ ├── ComposeNavGraph.kt │ │ │ ├── CoroutineExceptionsNavGraph.kt │ │ │ ├── CoroutineNavGraph.kt │ │ │ └── MainNavGraph.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ └── picture1.jpg │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── raw │ │ ├── amine_msiouri.jpg │ │ ├── cancel2.json │ │ ├── cancel3.json │ │ ├── connor_danylenko.jpg │ │ ├── fast_cancel.json │ │ ├── felix.jpg │ │ ├── julia_volk.jpg │ │ ├── lukas_dlutko.jpg │ │ ├── matthew_montrone.jpg │ │ ├── pixabay.jpg │ │ └── sam_willis.jpg │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── nick │ └── mirosh │ └── androidsamples │ ├── LaunchedEffectViewModelTest.kt │ └── MainCoroutineRule.kt ├── balls.ipynb ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── todo.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{kt,kts}] 4 | max_line_length = 100 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | misc.xml 22 | deploymentTargetDropDown.xml 23 | render.experimental.xml 24 | 25 | # Keystore files 26 | *.jks 27 | *.keystore 28 | 29 | # Google Services (e.g. APIs or Firebase) 30 | google-services.json 31 | 32 | # Android Profiling 33 | *.hprof 34 | -------------------------------------------------------------------------------- /ParallaxColumn/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ParallaxColumn/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | id ("maven-publish") 5 | } 6 | 7 | android { 8 | namespace = "nick.mirosh.parallaxcolumn" 9 | compileSdk = 33 10 | publishing { 11 | singleVariant("release") { 12 | withSourcesJar() 13 | } 14 | } 15 | defaultConfig { 16 | aarMetadata { 17 | minCompileSdk = 24 18 | } 19 | minSdk = 24 20 | 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | consumerProguardFiles("consumer-rules.pro") 23 | } 24 | 25 | buildTypes { 26 | release { 27 | isMinifyEnabled = false 28 | proguardFiles( 29 | getDefaultProguardFile("proguard-android-optimize.txt"), 30 | "proguard-rules.pro" 31 | ) 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_1_8 36 | targetCompatibility = JavaVersion.VERSION_1_8 37 | } 38 | kotlinOptions { 39 | jvmTarget = "1.8" 40 | } 41 | } 42 | 43 | publishing { 44 | publications { 45 | register("release") { 46 | groupId = "nick.mirosh" 47 | artifactId = "parallaxcolumn" 48 | version = "1.0" 49 | 50 | afterEvaluate { 51 | from(components["release"]) 52 | } 53 | } 54 | } 55 | } 56 | 57 | dependencies { 58 | 59 | implementation(platform("androidx.compose:compose-bom:2023.03.00")) 60 | implementation("androidx.compose.ui:ui") 61 | implementation("androidx.activity:activity-compose:1.7.2") 62 | implementation("androidx.compose.material:material:1.5.0") 63 | implementation("androidx.core:core-ktx:1.10.1") 64 | implementation("androidx.appcompat:appcompat:1.6.1") 65 | implementation("com.google.android.material:material:1.9.0") 66 | implementation("androidx.compose.foundation:foundation-layout-android:1.5.0") 67 | testImplementation("junit:junit:4.13.2") 68 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 69 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 70 | } -------------------------------------------------------------------------------- /ParallaxColumn/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/ParallaxColumn/consumer-rules.pro -------------------------------------------------------------------------------- /ParallaxColumn/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /ParallaxColumn/src/androidTest/java/nick/mirosh/parallaxcolumn/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.parallaxcolumn 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("nick.mirosh.parallaxcolumn.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /ParallaxColumn/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ParallaxColumn/src/main/java/nick/mirosh/parallaxcolumn/ImageUtils.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.parallaxcolumn 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.graphics.Bitmap 6 | import android.graphics.BitmapFactory 7 | import android.util.Log 8 | import android.util.TypedValue 9 | import kotlinx.coroutines.Deferred 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.async 12 | import kotlinx.coroutines.awaitAll 13 | import kotlinx.coroutines.withContext 14 | import java.io.FileInputStream 15 | import java.io.OutputStream 16 | import java.net.HttpURLConnection 17 | import java.net.URL 18 | 19 | 20 | fun downloadImage( 21 | imageUrl: String, 22 | ) = try { 23 | val url = URL(imageUrl) 24 | val connection = url.openConnection() as HttpURLConnection 25 | connection.doInput = true 26 | connection.connect() 27 | 28 | val inputStream = connection.inputStream 29 | BitmapFactory.decodeStream(inputStream) 30 | } catch (e: Exception) { 31 | e.printStackTrace() 32 | null 33 | } 34 | 35 | fun saveImageToInternalStorage( 36 | context: Context, 37 | bitmap: Bitmap, 38 | imageName: String 39 | ) { 40 | try { 41 | val fos: OutputStream = 42 | context.openFileOutput(imageName, Context.MODE_PRIVATE) 43 | bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos) 44 | fos.close() 45 | } catch (e: Exception) { 46 | e.printStackTrace() 47 | Log.e("ParallaxScreen", "saveImageToInternalStorage: failure") 48 | Log.e("ParallaxScreen", "${e.message}") 49 | } 50 | } 51 | 52 | fun decodeImageFromInternalStorage( 53 | context: Context, 54 | imageName: String 55 | ): Bitmap? { 56 | return try { 57 | val fis: FileInputStream = context.openFileInput(imageName) 58 | BitmapFactory.decodeStream(fis) 59 | } catch (e: Exception) { 60 | e.printStackTrace() 61 | null 62 | } 63 | } 64 | 65 | fun downloadPictureToInternalStorage( 66 | fileName: String, 67 | url: String, 68 | context: Context 69 | ) { 70 | downloadImage( 71 | imageUrl = url, 72 | )?.let { 73 | saveImageToInternalStorage(context = context, bitmap = it, imageName = fileName) 74 | } 75 | } 76 | 77 | fun decodeRawResource(resources: Resources, pictureId: Int): Bitmap? { 78 | val opts = BitmapFactory.Options().apply { 79 | inScaled = 80 | false // ensure the bitmap is not scaled based on device density 81 | } 82 | val inputStream = resources.openRawResource(pictureId) 83 | return BitmapFactory.decodeResourceStream( 84 | resources, 85 | TypedValue(), 86 | inputStream, 87 | null, 88 | opts 89 | ) 90 | } 91 | 92 | suspend fun loadPictures( 93 | pictureUri: List, 94 | context: Context, 95 | ) = 96 | withContext(Dispatchers.IO) { 97 | mutableListOf>().apply { 98 | pictureUri.forEach { 99 | add(async { 100 | when (it) { 101 | is PictureUri.RemoteUrl -> context.getPictureWithUrl(it.value) 102 | is PictureUri.RawResource -> context.loadLocalPictures(it.value) 103 | } 104 | }) 105 | } 106 | }.awaitAll() 107 | } 108 | 109 | fun calculateYOffset( 110 | totalColumnScrollFromTop: Int, cardHeight: Int, pictureHeight: Int 111 | ) = if (totalColumnScrollFromTop <= 0) { 112 | pictureHeight - cardHeight 113 | } 114 | else if (totalColumnScrollFromTop + cardHeight >= pictureHeight) { 115 | 0 116 | } 117 | else { 118 | pictureHeight - cardHeight - (totalColumnScrollFromTop) 119 | } 120 | 121 | fun Context.loadLocalPictures(pictureId: Int) = 122 | decodeRawResource( 123 | this.resources, pictureId 124 | ) 125 | 126 | 127 | fun Context.getPictureWithUrl(url: String): Bitmap? { 128 | val fileName = "img_${System.currentTimeMillis()}" 129 | downloadPictureToInternalStorage( 130 | fileName = fileName, 131 | url = url, 132 | context = this, 133 | ) 134 | val bitmap = decodeImageFromInternalStorage( 135 | this, 136 | fileName 137 | ) 138 | Log.d("ParallaxScreen", "bitmap.size = ${bitmap?.byteCount} bytes") 139 | return bitmap 140 | } 141 | -------------------------------------------------------------------------------- /ParallaxColumn/src/main/java/nick/mirosh/parallaxcolumn/ParallaxColumn.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.parallaxcolumn 2 | 3 | import android.content.res.Configuration 4 | import android.graphics.Bitmap 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.BoxScope 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.rememberScrollState 14 | import androidx.compose.foundation.verticalScroll 15 | import androidx.compose.material.Card 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.LaunchedEffect 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableIntStateOf 20 | import androidx.compose.runtime.mutableStateListOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 25 | import androidx.compose.ui.graphics.nativeCanvas 26 | import androidx.compose.ui.platform.LocalContext 27 | import androidx.compose.ui.platform.LocalDensity 28 | import androidx.compose.ui.platform.testTag 29 | import androidx.compose.ui.unit.Dp 30 | import androidx.compose.ui.unit.dp 31 | 32 | var screenWidthPx = 0 33 | var screenHeightPx = 0 34 | val defaulCardHeightDp = 200.dp 35 | val defaultParallaxScrollSpeed = 0.5f 36 | 37 | /** 38 | * @param pictureUris - URLs of pictures to be downloaded via network or R.raw.id's 39 | * to be loaded from the "raw" folder and drawn on top of the inverted card background 40 | * @param cardHeightInDp - height of the card in density pixels 41 | * @param parallaxScrollSpeed - speed of the parallax effect relative to the column scroll speed 42 | * @param content - content to be drawn on top of the card 43 | */ 44 | sealed class PictureUri { 45 | data class RemoteUrl(val value: String) : PictureUri() 46 | data class RawResource(val value: Int) : PictureUri() 47 | } 48 | 49 | @Composable 50 | fun UriInvertedParallaxColumn( 51 | modifier: Modifier = Modifier, 52 | pictureUris: List, 53 | cardHeight: Dp = defaulCardHeightDp, 54 | parallaxScrollSpeed: Float = defaultParallaxScrollSpeed, 55 | content: @Composable BoxScope.(index: Int) -> Unit, 56 | ) { 57 | 58 | val parsedBitmaps = remember { 59 | mutableStateListOf(null) 60 | } 61 | 62 | val context = LocalContext.current 63 | LaunchedEffect(Unit) { 64 | parsedBitmaps.removeFirstOrNull() 65 | parsedBitmaps.addAll( 66 | loadPictures( 67 | pictureUris, context 68 | ) 69 | ) 70 | } 71 | 72 | val bitmaps = parsedBitmaps.toList().filterNotNull() 73 | if (bitmaps.isNotEmpty()) { 74 | InvertedParallaxColumn( 75 | modifier = modifier, 76 | bitmaps = bitmaps, 77 | cardHeight = cardHeight, 78 | parallaxScrollSpeed = parallaxScrollSpeed, 79 | content = content 80 | ) 81 | } 82 | } 83 | 84 | val defaultCanvasModifier = Modifier 85 | 86 | val defaultColumnModifier = Modifier 87 | .fillMaxWidth() 88 | 89 | @Composable 90 | fun InvertedParallaxColumn( 91 | modifier: Modifier = defaultColumnModifier, 92 | cardModifier: Modifier = Modifier, 93 | bitmaps: List, 94 | spacerHeight: Dp = 16.dp, 95 | cardHeight: Dp = defaulCardHeightDp, 96 | parallaxScrollSpeed: Float = defaultParallaxScrollSpeed, 97 | content: @Composable BoxScope.(index: Int) -> Unit, 98 | ) { 99 | val columnScrollState = rememberScrollState() 100 | 101 | var prevScrollValue by remember { mutableIntStateOf(0) } 102 | val columnScrollFromTopInPx = columnScrollState.value 103 | prevScrollValue = columnScrollState.value 104 | Column( 105 | modifier = modifier 106 | .verticalScroll(columnScrollState) 107 | .testTag("column"), 108 | ) { 109 | repeat(bitmaps.size) { index -> 110 | Spacer(modifier = Modifier.height(spacerHeight)) 111 | InvertedCard( 112 | cardModifier = cardModifier, 113 | originalBitmap = bitmaps[index], 114 | cardHeight = cardHeight, 115 | totalColumnScrollFromTop = columnScrollFromTopInPx, 116 | parallaxScrollSpeed = parallaxScrollSpeed 117 | ) { 118 | content(index) 119 | } 120 | } 121 | } 122 | } 123 | 124 | @Composable 125 | fun InvertedCard( 126 | cardModifier: Modifier = Modifier, 127 | originalBitmap: Bitmap, 128 | cardHeight: Dp = defaulCardHeightDp, 129 | parallaxScrollSpeed: Float, 130 | totalColumnScrollFromTop: Int = 0, 131 | content: @Composable BoxScope.() -> Unit 132 | ) { 133 | val cardHeightInPx = with(LocalDensity.current) { 134 | cardHeight.roundToPx() 135 | } 136 | Card( 137 | modifier = cardModifier 138 | .height(cardHeight) 139 | .padding(start = 16.dp, end = 16.dp), 140 | ) { 141 | Box( 142 | modifier = cardModifier 143 | .fillMaxWidth() 144 | ) { 145 | Canvas( 146 | modifier = defaultCanvasModifier 147 | ) { 148 | drawIntoCanvas { canvas -> 149 | val width = originalBitmap.width 150 | val height = originalBitmap.height 151 | val yOffset = calculateYOffset( 152 | (totalColumnScrollFromTop * parallaxScrollSpeed).toInt(), 153 | cardHeightInPx, 154 | height 155 | ) 156 | val newBitmap = Bitmap.createBitmap( 157 | originalBitmap, 0, yOffset, width, cardHeightInPx 158 | ) 159 | canvas.nativeCanvas.drawBitmap( 160 | newBitmap, 0f, 0f, null 161 | ) 162 | } 163 | } 164 | content() 165 | } 166 | } 167 | } 168 | 169 | fun initScreenWidthAndHeight(configuration: Configuration, density: Float) { 170 | val screenHeightDp = configuration.screenHeightDp 171 | val screenWidthDp = configuration.screenWidthDp 172 | screenHeightPx = (screenHeightDp * density).toInt() 173 | screenWidthPx = (screenWidthDp * density).toInt() 174 | } 175 | -------------------------------------------------------------------------------- /ParallaxColumn/src/test/java/nick/mirosh/parallaxcolumn/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.parallaxcolumn 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | kotlin("kapt") 5 | id("com.google.dagger.hilt.android") 6 | } 7 | 8 | android { 9 | namespace = "nick.mirosh.androidsamples" 10 | compileSdk = 35 11 | 12 | defaultConfig { 13 | applicationId = "nick.mirosh.androidsamples" 14 | minSdk = 24 15 | targetSdk = 35 16 | versionCode = 1 17 | versionName = "1.0" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary = true 22 | } 23 | } 24 | buildFeatures.buildConfig = true 25 | buildTypes { 26 | debug { 27 | buildConfigField( 28 | "String", 29 | "BASE_URL", 30 | "\"https://pokeapi.co/api/v2/\"" 31 | ) 32 | } 33 | release { 34 | 35 | buildConfigField( 36 | "String", 37 | "BASE_URL", 38 | "\"https://pokeapi.co/api/v2/\"" 39 | ) 40 | 41 | isMinifyEnabled = false 42 | proguardFiles( 43 | getDefaultProguardFile("proguard-android-optimize.txt"), 44 | "proguard-rules.pro" 45 | ) 46 | signingConfig = signingConfigs.getByName("debug") 47 | } 48 | } 49 | compileOptions { 50 | sourceCompatibility = JavaVersion.VERSION_17 51 | targetCompatibility = JavaVersion.VERSION_17 52 | } 53 | kotlinOptions { 54 | jvmTarget = "17" 55 | languageVersion = "1.9" 56 | } 57 | kotlin.sourceSets.all { 58 | this.languageSettings.enableLanguageFeature("DataObjects") 59 | } 60 | buildFeatures { 61 | compose = true 62 | } 63 | composeOptions { 64 | kotlinCompilerExtensionVersion = "1.5.11" 65 | } 66 | } 67 | 68 | dependencies { 69 | 70 | implementation("androidx.core:core-ktx:1.15.0") 71 | implementation("androidx.activity:activity-compose:1.9.3") 72 | implementation(platform("androidx.compose:compose-bom:2024.10.01")) 73 | implementation("androidx.compose.ui:ui") 74 | implementation("androidx.compose.ui:ui-graphics") 75 | implementation("androidx.compose.ui:ui-tooling-preview") 76 | 77 | val retrofit = "2.11.0" 78 | implementation("com.squareup.retrofit2:retrofit:$retrofit") 79 | //gson converter factory dependency 80 | implementation("com.squareup.retrofit2:converter-gson:$retrofit") 81 | implementation("com.squareup.okhttp3:okhttp:4.12.0") 82 | implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") 83 | 84 | implementation("androidx.navigation:navigation-compose:2.8.3") 85 | 86 | implementation("androidx.hilt:hilt-navigation-compose:1.2.0") 87 | implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0") 88 | val hilt = "2.52" 89 | implementation("com.google.dagger:hilt-android:$hilt") 90 | kapt("com.google.dagger:hilt-android-compiler:$hilt") 91 | implementation("androidx.compose.material3:material3:1.3.1") 92 | implementation("androidx.compose.material:material:1.7.5") 93 | 94 | 95 | implementation ("com.airbnb.android:lottie-compose:6.4.1") 96 | 97 | val coroutines = "1.9.0" 98 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines") 99 | testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines") 100 | 101 | // ViewModel 102 | val lifecycle = "2.8.7" 103 | 104 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle") 105 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle") 106 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle") 107 | implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle") 108 | 109 | implementation("io.coil-kt:coil-compose:2.6.0") 110 | 111 | val uiTest = "1.7.5" 112 | androidTestImplementation("androidx.compose.ui:ui-test-junit4:$uiTest") 113 | debugImplementation("androidx.compose.ui:ui-test-manifest:$uiTest") 114 | testImplementation("junit:junit:4.13.2") 115 | 116 | androidTestImplementation("androidx.test.ext:junit:1.2.1") 117 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") 118 | androidTestImplementation(platform("androidx.compose:compose-bom:2024.10.01")) 119 | androidTestImplementation("androidx.compose.ui:ui-test-junit4") 120 | debugImplementation("androidx.compose.ui:ui-tooling") 121 | debugImplementation("androidx.compose.ui:ui-test-manifest") 122 | 123 | val room_version = "2.6.1" 124 | 125 | implementation("androidx.room:room-runtime:$room_version") 126 | annotationProcessor("androidx.room:room-compiler:$room_version") 127 | kapt("androidx.room:room-compiler:$room_version") 128 | implementation("androidx.room:room-ktx:$room_version") 129 | 130 | implementation("com.github.nsmirosh:ParallaxColumn:1.0.4") 131 | } 132 | kapt { 133 | correctErrorTypes = true 134 | } 135 | 136 | tasks.register("checkManifest") { 137 | doLast { 138 | val manifestFile = android.sourceSets["main"].manifest.srcFile 139 | val manifestContent = manifestFile.readText() 140 | if (manifestContent.contains("""screenOrientation="portrait"""") || 141 | manifestContent.contains("""screenOrientation="landscape"""") 142 | ) { 143 | throw GradleException( 144 | "A screen orientation is locked inside an Activity. " + 145 | "This is not allowed." 146 | ) 147 | } 148 | } 149 | } 150 | 151 | tasks.build { 152 | dependsOn("checkManifest") 153 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/SideEffectsScreen.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.test.assertIsDisplayed 2 | import androidx.compose.ui.test.junit4.createComposeRule 3 | import androidx.compose.ui.test.onNodeWithTag 4 | import androidx.compose.ui.test.onNodeWithText 5 | import androidx.compose.ui.test.performClick 6 | import androidx.compose.ui.test.performTextInput 7 | import com.example.androidcomposeexample.ui.sideeffects.launchedeffect.CHECKBOX_TAG 8 | import com.example.androidcomposeexample.ui.sideeffects.launchedeffect.LaunchedEffectScreen 9 | import com.example.androidcomposeexample.ui.sideeffects.launchedeffect.MESSAGE_INPUT_TAG 10 | import com.example.androidcomposeexample.ui.sideeffects.launchedeffect.TIMER_UPDATE_TAG 11 | import nick.mirosh.androidsamples.ui.theme.MyApplicationTheme 12 | import org.junit.Rule 13 | import org.junit.Test 14 | 15 | class LaunchedEffectScreenTest { 16 | 17 | @get:Rule 18 | val composeTestRule = createComposeRule() 19 | 20 | @Test 21 | fun clickingThroughTheFlow_withValidInputs_showsTimer() { 22 | // Start the app 23 | composeTestRule.setContent { 24 | MyApplicationTheme { 25 | LaunchedEffectScreen() 26 | } 27 | } 28 | composeTestRule.onNodeWithTag(MESSAGE_INPUT_TAG) 29 | .performTextInput("First message") 30 | composeTestRule.onNodeWithText("Schedule message").performClick() 31 | composeTestRule.onNodeWithTag(MESSAGE_INPUT_TAG) 32 | .performTextInput("Update message") 33 | composeTestRule.onNodeWithText("Schedule message").performClick() 34 | composeTestRule.onNodeWithTag(TIMER_UPDATE_TAG).assertIsDisplayed() 35 | } 36 | 37 | @Test 38 | fun clickingThroughTheFlow_withValidInputsAndCheckBox_showsTimer() { 39 | // Start the app 40 | composeTestRule.setContent { 41 | MyApplicationTheme { 42 | LaunchedEffectScreen() 43 | } 44 | } 45 | composeTestRule.onNodeWithTag(MESSAGE_INPUT_TAG) 46 | 47 | .performTextInput("First message") 48 | composeTestRule.onNodeWithText("Schedule message").performClick() 49 | composeTestRule.onNodeWithTag(MESSAGE_INPUT_TAG) 50 | .performTextInput("Update message") 51 | composeTestRule.onNodeWithTag(CHECKBOX_TAG).performClick() 52 | composeTestRule.onNodeWithText("Schedule message").performClick() 53 | composeTestRule.onNodeWithTag(TIMER_UPDATE_TAG).assertIsDisplayed() 54 | } 55 | 56 | @Test 57 | fun checkOrientation() { 58 | 59 | // composeTestRule.ac 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/amine_msiouri.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/androidTest/res/raw/amine_msiouri.jpg -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/connor_danylenko.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/androidTest/res/raw/connor_danylenko.jpg -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/felix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/androidTest/res/raw/felix.jpg -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/julia_volk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/androidTest/res/raw/julia_volk.jpg -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/lukas_dlutko.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/androidTest/res/raw/lukas_dlutko.jpg -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/matthew_montrone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/androidTest/res/raw/matthew_montrone.jpg -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/pixabay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/androidTest/res/raw/pixabay.jpg -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/sam_willis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/androidTest/res/raw/sam_willis.jpg -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.ui.Modifier 10 | import androidx.navigation.NavGraphBuilder 11 | import androidx.navigation.NavHostController 12 | import androidx.navigation.compose.NavHost 13 | import androidx.navigation.compose.composable 14 | import androidx.navigation.compose.rememberNavController 15 | import dagger.hilt.android.AndroidEntryPoint 16 | import nick.mirosh.androidsamples.ui.android.AndroidApisScreen 17 | import nick.mirosh.androidsamples.ui.coroutines.lobby.CoroutineLobbyScreen 18 | import nick.mirosh.androidsamples.ui.jetpack_compose.lobby.ComposeLobbyScreen 19 | import nick.mirosh.androidsamples.ui.main.MainScreenContent 20 | import nick.mirosh.androidsamples.ui.navigation.AndroidApis 21 | import nick.mirosh.androidsamples.ui.navigation.Compose 22 | import nick.mirosh.androidsamples.ui.navigation.Coroutines 23 | import nick.mirosh.androidsamples.ui.navigation.MainScreen 24 | import nick.mirosh.androidsamples.ui.theme.MyApplicationTheme 25 | 26 | @AndroidEntryPoint 27 | class MainActivity : ComponentActivity() { 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContent { 31 | MyApplicationTheme { 32 | val navController = rememberNavController() 33 | Surface( 34 | modifier = Modifier.fillMaxSize(), 35 | color = MaterialTheme.colorScheme.background 36 | ) { 37 | NavHost( 38 | navController = navController, 39 | startDestination = MainScreen.route, 40 | modifier = Modifier 41 | ) { 42 | setUpNavigation(navController) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | fun NavGraphBuilder.setUpNavigation(navController: NavHostController) { 51 | 52 | composable(route = MainScreen.route) { 53 | MainScreenContent( 54 | onCoroutinesClicked = { 55 | navController.navigateSingleTopTo( 56 | Coroutines.route 57 | ) 58 | }, 59 | onComposeClicked = { 60 | navController.navigateSingleTopTo( 61 | Compose.route 62 | ) 63 | }, 64 | 65 | onAndroidApisClicked = { 66 | navController.navigateSingleTopTo( 67 | AndroidApis.route 68 | ) 69 | } 70 | ) 71 | } 72 | 73 | composable(route = Coroutines.route) { 74 | CoroutineLobbyScreen() 75 | } 76 | composable(route = Compose.route) { 77 | ComposeLobbyScreen() 78 | } 79 | 80 | composable(route = AndroidApis.route) { 81 | AndroidApisScreen() 82 | } 83 | } 84 | 85 | fun NavHostController.navigateSingleTopTo(route: String) = 86 | this.navigate(route) { launchSingleTop = true } 87 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/PokemonApi.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples 2 | 3 | import nick.mirosh.androidsamples.responses.PokemonResponse 4 | import retrofit2.Call 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | 8 | interface PokemonApiService { 9 | 10 | @GET("pokemon") 11 | fun getPokemon( 12 | ): Call 13 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/UrlUtils.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples 2 | 3 | fun getImageUrl(pokemonUrl: String): String { 4 | 5 | val regex = Regex("\\d+(?=/[^/]*$)") 6 | val matches = regex.findAll(pokemonUrl) 7 | val id = matches.lastOrNull()?.value 8 | return "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$id.png" 9 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/database/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import nick.mirosh.androidsamples.models.DatabaseTodo 6 | 7 | const val DATABASE_NAME = "todos-db" 8 | 9 | @Database(entities = [DatabaseTodo::class], version = 1) 10 | abstract class AppDatabase : RoomDatabase() { 11 | abstract fun todoDao(): TodoDao 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/database/ArticleDao.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.database 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import kotlinx.coroutines.flow.Flow 9 | import nick.mirosh.androidsamples.models.DatabaseTodo 10 | 11 | @Dao 12 | interface TodoDao { 13 | @Query("SELECT * FROM todos") 14 | fun getAllTodos(): Flow> 15 | 16 | @Query("SELECT * FROM todos WHERE completed = 1") 17 | fun getCompletedTodos(): List 18 | 19 | @Insert(onConflict = OnConflictStrategy.IGNORE) 20 | fun insertAll(todos: List): List 21 | 22 | @Insert(onConflict = OnConflictStrategy.REPLACE) 23 | fun insert(todo: DatabaseTodo) 24 | 25 | @Delete 26 | fun delete(todo: DatabaseTodo) 27 | 28 | @Query("DELETE FROM todos WHERE id = :todoId") 29 | fun deleteById(todoId: Int) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/di/CoroutinesModule.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | 10 | import javax.inject.Qualifier 11 | 12 | @Retention(AnnotationRetention.BINARY) 13 | @Qualifier 14 | annotation class DefaultDispatcher 15 | 16 | @Retention(AnnotationRetention.BINARY) 17 | @Qualifier 18 | annotation class IoDispatcher 19 | 20 | @Retention(AnnotationRetention.BINARY) 21 | @Qualifier 22 | annotation class MainDispatcher 23 | 24 | @Retention(AnnotationRetention.BINARY) 25 | @Qualifier 26 | annotation class MainImmediateDispatcher 27 | 28 | @InstallIn(SingletonComponent::class) 29 | @Module 30 | class CoroutinesModule { 31 | 32 | @DefaultDispatcher 33 | @Provides 34 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default 35 | 36 | @IoDispatcher 37 | @Provides 38 | fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO 39 | 40 | @MainDispatcher 41 | @Provides 42 | fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main 43 | 44 | @MainImmediateDispatcher 45 | @Provides 46 | fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import nick.mirosh.androidsamples.database.AppDatabase 11 | import nick.mirosh.androidsamples.database.DATABASE_NAME 12 | import nick.mirosh.androidsamples.database.TodoDao 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | class DatabaseModule { 18 | 19 | @Singleton 20 | @Provides 21 | fun provideAppDatabase(@ApplicationContext appContext: Context): TodoDao { 22 | return Room 23 | .databaseBuilder(appContext, AppDatabase::class.java, DATABASE_NAME) 24 | .build() 25 | .todoDao() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.android.components.ViewModelComponent 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import nick.mirosh.androidsamples.PokemonApiService 9 | import nick.mirosh.androidsamples.database.TodoDao 10 | import nick.mirosh.androidsamples.repository.PokemonRepository 11 | import nick.mirosh.androidsamples.repository.PokemonRepositoryImpl 12 | import nick.mirosh.androidsamples.repository.TodoRepository 13 | import nick.mirosh.androidsamples.repository.TodoRepositoryImpl 14 | 15 | @Module 16 | @InstallIn(ViewModelComponent::class) 17 | class RepositoryModule { 18 | 19 | @Provides 20 | fun provideRepository( 21 | pokemonApiService: PokemonApiService 22 | ): PokemonRepository { 23 | return PokemonRepositoryImpl( 24 | pokemonApiService 25 | ) 26 | } 27 | 28 | @Provides 29 | fun provideTodoRepository( 30 | @IoDispatcher dispatcher: CoroutineDispatcher, 31 | todoDao: TodoDao 32 | ): TodoRepository { 33 | return TodoRepositoryImpl( 34 | dispatcher, 35 | todoDao 36 | ) 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/di/RetrofitModule.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.android.components.ViewModelComponent 7 | import nick.mirosh.androidsamples.BuildConfig 8 | import nick.mirosh.androidsamples.PokemonApiService 9 | import nick.mirosh.androidsamples.networking.HeaderInterceptor 10 | import okhttp3.OkHttpClient 11 | import okhttp3.logging.HttpLoggingInterceptor 12 | import retrofit2.Retrofit 13 | import retrofit2.converter.gson.GsonConverterFactory 14 | 15 | @Module 16 | @InstallIn(ViewModelComponent::class) 17 | class RetrofitModule { 18 | 19 | @Provides 20 | fun providePokemonService( 21 | okHttpClient: OkHttpClient 22 | ): PokemonApiService { 23 | 24 | val retrofit = Retrofit.Builder() 25 | .baseUrl(BuildConfig.BASE_URL) 26 | .addConverterFactory(GsonConverterFactory.create()) 27 | .client(okHttpClient) 28 | .build() 29 | 30 | return retrofit.create(PokemonApiService::class.java) 31 | } 32 | 33 | @Provides 34 | fun provideOkHttpWithLogger( 35 | ): OkHttpClient { 36 | return OkHttpClient.Builder() 37 | .addInterceptor(HttpLoggingInterceptor().apply { 38 | level = HttpLoggingInterceptor.Level.BODY 39 | }) 40 | .addInterceptor(HeaderInterceptor()) 41 | .build() 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/dto/PokemonResultDto.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.dto 2 | 3 | data class PokemonResultDto( 4 | val name: String? = null, 5 | val url: String? = null, 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/models/DatabaseTodo.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.models 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "todos") 7 | data class DatabaseTodo( 8 | @PrimaryKey(autoGenerate = true) val id: Int? = null, 9 | val title: String, val description: String, val completed: Boolean = false, 10 | ) 11 | 12 | fun DatabaseTodo.asDomainModel() = Todo( 13 | id = id ?: 0, 14 | title = title, 15 | description = description, 16 | completed = completed 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/models/Pokemon.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.models 2 | 3 | data class Pokemon( 4 | val name: String, 5 | val url: String, 6 | var isExpanded: Boolean = false, 7 | var color: Int = 0 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/models/Todo.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.models 2 | 3 | data class Todo( 4 | val id: Int, 5 | val title: String, 6 | val description: String, 7 | val completed: Boolean 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/networking/HeaderInterceptor.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.networking 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.Response 5 | 6 | class HeaderInterceptor : Interceptor { 7 | override fun intercept(chain: Interceptor.Chain): Response = chain.run { 8 | proceed( 9 | request() 10 | .newBuilder() 11 | //.addHeader("X-Api-Key", BuildConfig.API_KEY) 12 | .build() 13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/repository/PokemonRepository.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.repository 2 | 3 | import nick.mirosh.androidsamples.models.Pokemon 4 | 5 | interface PokemonRepository { 6 | suspend fun getPokemon(): List 7 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/repository/PokemonRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.repository 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import nick.mirosh.androidsamples.PokemonApiService 6 | import nick.mirosh.androidsamples.models.Pokemon 7 | import javax.inject.Inject 8 | 9 | class PokemonRepositoryImpl @Inject constructor(private val pokemonApi: PokemonApiService) : 10 | PokemonRepository { 11 | 12 | override suspend fun getPokemon(): List { 13 | return withContext(Dispatchers.IO) { 14 | pokemonApi.getPokemon().execute().body()?.let { pokemonResponse -> 15 | pokemonResponse.results?.map { 16 | Pokemon(it.name.orEmpty(), it.url.orEmpty(), false) 17 | } ?: listOf() 18 | }!! 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/repository/TodoRepository.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import nick.mirosh.androidsamples.models.DatabaseTodo 5 | 6 | interface TodoRepository { 7 | suspend fun getTodos(): Flow> 8 | suspend fun insert(todo: DatabaseTodo) 9 | suspend fun delete(todoId: Int) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/repository/TodoRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.repository 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.withContext 6 | import nick.mirosh.androidsamples.database.TodoDao 7 | import nick.mirosh.androidsamples.di.IoDispatcher 8 | import nick.mirosh.androidsamples.models.DatabaseTodo 9 | import javax.inject.Inject 10 | 11 | class TodoRepositoryImpl @Inject constructor( 12 | @IoDispatcher private val dispatcher: CoroutineDispatcher, 13 | private val todoDao: TodoDao 14 | ) : TodoRepository { 15 | 16 | override suspend fun getTodos(): Flow> { 17 | return withContext(dispatcher) { 18 | todoDao.getAllTodos() 19 | } 20 | } 21 | 22 | override suspend fun insert(todo: DatabaseTodo) { 23 | withContext(dispatcher) { 24 | todoDao.insert(todo) 25 | } 26 | } 27 | 28 | override suspend fun delete(todoId: Int) { 29 | withContext(dispatcher) { 30 | todoDao.deleteById(todoId) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/responses/PokemonResponse.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.responses 2 | 3 | import nick.mirosh.androidsamples.dto.PokemonResultDto 4 | 5 | data class PokemonResponse( 6 | val count: Int? = null, 7 | val next: String? = null, 8 | val previous: String? = null, 9 | val results: List? = null 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class MyApplication : Application() { 8 | 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/RecompositionDestinations.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package nick.mirosh.androidsamples.ui 18 | 19 | interface RecompositionDestinations { 20 | val route: String 21 | } 22 | 23 | object RecompositionLobbyScreen : RecompositionDestinations { 24 | override val route = "recomposition_lobby_screen" 25 | } 26 | 27 | object RecompositionList : RecompositionDestinations { 28 | override val route = "recomposition_list" 29 | } 30 | 31 | object SimpleRecomposition : RecompositionDestinations { 32 | override val route = "simple_recomposition" 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/android/AndroidApisScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.android 2 | 3 | import android.app.AlarmManager 4 | import android.app.PendingIntent 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.os.Build 9 | import android.os.SystemClock 10 | import android.util.Log 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.platform.LocalContext 13 | 14 | 15 | @Composable 16 | fun AndroidApisScreen() { 17 | 18 | val context = LocalContext.current.applicationContext 19 | scheduleElapsedRealtimeTask(context) 20 | } 21 | 22 | class YourElapsedReceiver( 23 | ) : BroadcastReceiver() { 24 | override fun onReceive(context: Context?, intent: Intent?) { 25 | 26 | Log.d("YourElapsedReceiver", "onReceive: ") 27 | if (intent?.action == "Balls") { 28 | Log.d("YourElapsedReceiver", "onReceive balls: ") 29 | // repo.getUserInfo() 30 | } 31 | } 32 | } 33 | 34 | private fun scheduleElapsedRealtimeTask(context: Context) { 35 | 36 | val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 37 | val intent = Intent(context, YourElapsedReceiver::class.java) 38 | intent.action = "Balls" 39 | val requestCode = 0 40 | 41 | // If task is already scheduled, return 42 | if (PendingIntent.getBroadcast( 43 | context, requestCode, intent, 44 | PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE 45 | ) != null 46 | ) { 47 | return 48 | } 49 | 50 | val pendingIntent = PendingIntent.getBroadcast( 51 | context, 52 | requestCode, 53 | intent, 54 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 55 | ) 56 | 57 | // val triggerAtMillis = SystemClock.elapsedRealtime() + timeLeftInSeconds * 1000 58 | val triggerAtMillis = SystemClock.elapsedRealtime() + 5 * 1000 59 | 60 | try { 61 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 62 | alarmManager.setExactAndAllowWhileIdle( 63 | AlarmManager.ELAPSED_REALTIME_WAKEUP, 64 | triggerAtMillis, 65 | pendingIntent 66 | ) 67 | } else { 68 | alarmManager.setExact( 69 | AlarmManager.ELAPSED_REALTIME_WAKEUP, 70 | triggerAtMillis, 71 | pendingIntent 72 | ) 73 | } 74 | } catch (e: SecurityException) { 75 | e.printStackTrace() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/background_processing/multiple_processes/MySameProcessService.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.background_processing.multiple_processes 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.os.IBinder 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.launch 10 | 11 | class MySameProcessService : Service() { 12 | override fun onBind(intent: Intent): IBinder? { 13 | return null 14 | } 15 | 16 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 17 | val causeCrash = intent.getBooleanExtra("cause_crash", false) 18 | Thread.setDefaultUncaughtExceptionHandler{ 19 | _, throwable -> throwable.printStackTrace() 20 | } 21 | if (causeCrash) { 22 | throw RuntimeException("Crash in same process service") 23 | } 24 | return START_NOT_STICKY 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/background_processing/multiple_processes/MySeparateProcessService.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.background_processing.multiple_processes 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.os.IBinder 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.launch 10 | 11 | class MySeparateProcessService : Service() { 12 | override fun onBind(intent: Intent): IBinder? { 13 | return null 14 | } 15 | 16 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 17 | val causeCrash = intent.getBooleanExtra("cause_crash", false) 18 | if (causeCrash) { 19 | throw RuntimeException("Crash in separate process service") 20 | } 21 | return START_NOT_STICKY 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/background_processing/multiple_processes/ProcessesScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.background_processing.multiple_processes 2 | 3 | import android.content.Intent 4 | import android.graphics.Color.parseColor 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.ButtonDefaults 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | 28 | 29 | @Composable 30 | fun ProcessesScreen( 31 | ) { 32 | val localContext = LocalContext.current 33 | 34 | MaterialTheme { 35 | Box( 36 | modifier = Modifier 37 | .background(Color.White) 38 | .fillMaxSize() 39 | ) { 40 | Column( 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | .align(Alignment.Center) 44 | .padding(16.dp) 45 | ) { 46 | var sameProcessState by remember { mutableStateOf(ProcessState.Idle) } 47 | var differentProcessState by remember { mutableStateOf(ProcessState.Idle) } 48 | 49 | Button( 50 | modifier = Modifier 51 | .padding(16.dp) 52 | .align(Alignment.CenterHorizontally), 53 | colors = ButtonDefaults.buttonColors( 54 | Color( 55 | parseColor( 56 | when (sameProcessState) { 57 | ProcessState.Idle -> "#00ab41" 58 | ProcessState.Started -> "#f96900" 59 | ProcessState.Crashed -> "#f90000" 60 | } 61 | ) 62 | ) 63 | ), 64 | onClick = { 65 | val intent = Intent( 66 | localContext, 67 | MySameProcessService::class.java 68 | ) 69 | sameProcessState = when (sameProcessState) { 70 | ProcessState.Idle -> 71 | ProcessState.Started 72 | 73 | ProcessState.Started -> { 74 | intent.putExtra("cause_crash", true) 75 | ProcessState.Crashed 76 | } 77 | 78 | ProcessState.Crashed -> 79 | ProcessState.Idle 80 | 81 | } 82 | localContext.startService(intent) 83 | } 84 | ) { 85 | Text( 86 | modifier = Modifier.padding(8.dp), 87 | text = when(sameProcessState) { 88 | ProcessState.Idle -> "Start service in same process" 89 | ProcessState.Started -> "Service is running. Click to crash." 90 | ProcessState.Crashed -> "Service crashed" 91 | } 92 | 93 | , fontSize = 18.sp 94 | ) 95 | } 96 | 97 | Button( 98 | 99 | modifier = Modifier 100 | .padding(16.dp) 101 | .align(Alignment.CenterHorizontally), 102 | 103 | colors = ButtonDefaults.buttonColors( 104 | Color( 105 | parseColor( 106 | when (differentProcessState) { 107 | ProcessState.Idle -> "#00ab41" 108 | ProcessState.Started -> "#f96900" 109 | ProcessState.Crashed -> "#f90000" 110 | } 111 | ) 112 | ) 113 | ), 114 | onClick = { 115 | val intent = Intent( 116 | localContext, 117 | MySeparateProcessService::class.java 118 | ) 119 | differentProcessState = when (differentProcessState) { 120 | ProcessState.Idle -> 121 | ProcessState.Started 122 | 123 | ProcessState.Started -> { 124 | intent.putExtra("cause_crash", true) 125 | ProcessState.Crashed 126 | } 127 | 128 | ProcessState.Crashed -> 129 | ProcessState.Idle 130 | 131 | } 132 | localContext.startService(intent) 133 | } 134 | ) { 135 | Text( 136 | modifier = Modifier.padding(8.dp), 137 | text = when(differentProcessState) { 138 | ProcessState.Idle -> "Start service in different process" 139 | ProcessState.Started -> "Service is running. Click to crash." 140 | ProcessState.Crashed -> "Service crashed" 141 | }, 142 | fontSize = 18.sp 143 | ) 144 | } 145 | } 146 | } 147 | 148 | } 149 | } 150 | 151 | sealed class ProcessState { 152 | data object Idle : ProcessState() 153 | data object Started : ProcessState() 154 | data object Crashed : ProcessState() 155 | } 156 | 157 | @Preview 158 | @Composable 159 | fun ProcessesScreenPreview() { 160 | ProcessesScreen() 161 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/async/AsyncComparisonScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.async 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.Button 10 | import androidx.compose.material.ButtonDefaults 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.key 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.tooling.preview.Preview 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import androidx.hilt.navigation.compose.hiltViewModel 26 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 27 | import nick.mirosh.androidsamples.ui.coroutines.ProgressBar 28 | import nick.mirosh.androidsamples.ui.coroutines.myGreen 29 | import nick.mirosh.androidsamples.ui.theme.MyApplicationTheme 30 | 31 | 32 | @Composable 33 | fun AsyncComparisonScreen( 34 | viewModel: AsyncComparisonViewModel = hiltViewModel() 35 | ) { 36 | val deferred1Updates by viewModel.deferred1Flow.collectAsStateWithLifecycle() 37 | val deferred2Updates by viewModel.deferred2Flow.collectAsStateWithLifecycle() 38 | val job1FlowUpdates by viewModel.job1flow.collectAsStateWithLifecycle() 39 | val job2FlowUpdates by viewModel.job2flow.collectAsStateWithLifecycle() 40 | var restart by remember { 41 | mutableStateOf(false) 42 | } 43 | MaterialTheme { 44 | key(restart) { 45 | Column { 46 | var asyncsLaunched by remember { 47 | mutableStateOf(false) 48 | } 49 | 50 | var coroutinesLaunched by remember { 51 | mutableStateOf(false) 52 | } 53 | Button( 54 | colors = if (asyncsLaunched) ButtonDefaults.buttonColors( 55 | backgroundColor = myGreen(), 56 | contentColor = Color.White 57 | ) 58 | else ButtonDefaults.buttonColors(), 59 | modifier = Modifier.padding(horizontal = 16.dp), 60 | onClick = { 61 | viewModel.launchAsyncs() 62 | asyncsLaunched = true 63 | } 64 | ) { 65 | Text(if (asyncsLaunched) "Asyncs Launched!" else "Launch asyncs") 66 | } 67 | Button( 68 | colors = if (coroutinesLaunched) ButtonDefaults.buttonColors( 69 | backgroundColor = myGreen(), 70 | contentColor = Color.White 71 | ) 72 | else ButtonDefaults.buttonColors(), 73 | modifier = Modifier.padding(horizontal = 16.dp), 74 | onClick = { 75 | viewModel.launchCoroutines() 76 | coroutinesLaunched = true 77 | } 78 | ) { 79 | Text(if (coroutinesLaunched) "Coroutines Launched!" else "Launch Coroutines") 80 | } 81 | 82 | ProgressBarWithCancel( 83 | progress = deferred1Updates, 84 | label = "Async #1", 85 | onCancelClick = { 86 | viewModel.cancelAsync1() 87 | } 88 | ) 89 | Column { 90 | 91 | } 92 | ProgressBarWithCancel( 93 | progress = deferred2Updates, 94 | label = "Async #2", 95 | onCancelClick = { 96 | viewModel.cancelAsync2() 97 | } 98 | ) 99 | ProgressBarWithCancel( 100 | progress = job1FlowUpdates, 101 | label = "Launch #1", 102 | onCancelClick = { 103 | viewModel.cancelCoroutine1() 104 | } 105 | ) 106 | ProgressBarWithCancel( 107 | progress = job2FlowUpdates, 108 | label = "Launch #2", 109 | onCancelClick = { 110 | viewModel.cancelCoroutine2() 111 | } 112 | ) 113 | Button( 114 | modifier = Modifier 115 | .align(Alignment.CenterHorizontally) 116 | .padding(top = 16.dp), 117 | onClick = { 118 | viewModel.clear() 119 | restart = !restart 120 | }) { 121 | Text("Restart") 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | 129 | @Composable 130 | fun ProgressBarWithCancel( 131 | modifier: Modifier = Modifier, 132 | progress: Float, 133 | label: String, 134 | onCancelClick: () -> Unit, 135 | ) { 136 | 137 | var cancelled by remember { 138 | mutableStateOf(false) 139 | } 140 | Column { 141 | Spacer(modifier = Modifier.height(16.dp)) 142 | Text( 143 | modifier = Modifier 144 | .padding(horizontal = 16.dp), 145 | text = label, 146 | fontSize = 20.sp 147 | ) 148 | Row( 149 | modifier = Modifier.fillMaxWidth(), 150 | verticalAlignment = Alignment.CenterVertically 151 | ) { 152 | ProgressBar( 153 | modifier = modifier 154 | .weight(2f / 3f), 155 | progress = progress 156 | ) 157 | 158 | Button( 159 | colors = if (cancelled) ButtonDefaults.buttonColors( 160 | backgroundColor = Color.Red, 161 | contentColor = Color.White 162 | ) 163 | else ButtonDefaults.buttonColors(), 164 | modifier = Modifier 165 | .weight(1f / 3f) 166 | .padding(horizontal = 16.dp), 167 | onClick = { 168 | cancelled = true 169 | onCancelClick() 170 | }) { 171 | Text(if (cancelled) "Cancelled" else "Cancel") 172 | } 173 | } 174 | } 175 | } 176 | 177 | 178 | @Preview 179 | @Composable 180 | fun AsyncComparisonScreenPreview() { 181 | MyApplicationTheme { 182 | AsyncComparisonScreen() 183 | } 184 | } 185 | 186 | @Preview 187 | @Composable 188 | fun ProgressBarWithCancelPreview() { 189 | MyApplicationTheme { 190 | ProgressBarWithCancel( 191 | modifier = Modifier.padding(16.dp), 192 | progress = 0.5f, 193 | label = "Deferred 1", 194 | onCancelClick = {} 195 | ) 196 | } 197 | } 198 | 199 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/async/AsyncComparisonViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.async 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.Deferred 7 | import kotlinx.coroutines.Job 8 | import kotlinx.coroutines.async 9 | import kotlinx.coroutines.awaitAll 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | import kotlinx.coroutines.joinAll 13 | import kotlinx.coroutines.launch 14 | import nick.mirosh.androidsamples.ui.coroutines.runUpdatesIn 15 | 16 | class AsyncComparisonViewModel : ViewModel() { 17 | 18 | private val _deferred1Flow = MutableStateFlow(0f) 19 | val deferred1Flow = _deferred1Flow.asStateFlow() 20 | 21 | private val _deferred2Flow = MutableStateFlow(0f) 22 | val deferred2Flow = _deferred2Flow.asStateFlow() 23 | 24 | private val _job1flow = MutableStateFlow(0f) 25 | val job1flow = _job1flow.asStateFlow() 26 | 27 | private val _job2flow = MutableStateFlow(0f) 28 | val job2flow = _job2flow.asStateFlow() 29 | 30 | private var deferred1: Deferred? = null 31 | private var deferred2: Deferred? = null 32 | private var job1: Job? = null 33 | private var job2: Job? = null 34 | 35 | fun launchAsyncs() { 36 | viewModelScope.launch { 37 | deferred1 = async { 38 | runUpdatesIn(_deferred1Flow) 39 | } 40 | deferred2 = async { 41 | runUpdatesIn(_deferred2Flow) 42 | } 43 | awaitAll(deferred1!!, deferred2!!) 44 | } 45 | } 46 | 47 | 48 | fun cancelAsync1() { 49 | deferred1?.cancel() 50 | } 51 | 52 | fun cancelAsync2() { 53 | deferred2?.cancel() 54 | } 55 | 56 | fun cancelCoroutine1() { 57 | Log.d("AsyncComparison", "cancelling coroutine 1") 58 | job1?.cancel() 59 | } 60 | 61 | fun cancelCoroutine2() { 62 | Log.d("AsyncComparison", "cancelling coroutine 2") 63 | job2?.cancel() 64 | } 65 | 66 | fun clear() { 67 | job1?.cancel() 68 | job2?.cancel() 69 | deferred1?.cancel() 70 | deferred2?.cancel() 71 | _deferred1Flow.value = 0f 72 | _deferred2Flow.value = 0f 73 | _job1flow.value = 0f 74 | _job2flow.value = 0f 75 | } 76 | 77 | fun launchCoroutines() { 78 | viewModelScope.launch { 79 | job1 = launch { 80 | runUpdatesIn(_job1flow) 81 | } 82 | 83 | job2 = launch { 84 | runUpdatesIn(_job2flow) 85 | } 86 | joinAll(job1!!, job2!!) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/cooperative_coroutine/CooperativeCancellationViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.cooperative_coroutine 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.CancellationException 7 | import kotlinx.coroutines.CoroutineStart 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.Job 10 | import kotlinx.coroutines.delay 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.asStateFlow 13 | import kotlinx.coroutines.isActive 14 | import kotlinx.coroutines.launch 15 | import kotlinx.coroutines.runBlocking 16 | 17 | const val TAG = "CooperativeCancellationViewModel" 18 | 19 | class CooperativeCancellationViewModel : ViewModel() { 20 | 21 | lateinit var job: Job 22 | 23 | var shouldRun = true 24 | 25 | private val _job1flow = MutableStateFlow(0f) 26 | val job1flow = _job1flow.asStateFlow() 27 | 28 | private val _coroutineStatus = MutableStateFlow("Not started") 29 | val coroutineStatus = _coroutineStatus.asStateFlow() 30 | 31 | init { 32 | setupUncooperativeCoroutine() 33 | } 34 | 35 | private fun setupUncooperativeCoroutine() { 36 | runBlocking { 37 | job = GlobalScope.launch( 38 | start = CoroutineStart.LAZY 39 | ) { 40 | _coroutineStatus.value = getCoroutineStatus(job) 41 | Log.d(TAG, "running uncooperative cooroutine") 42 | var counter = 0 43 | while (counter <= 100 && shouldRun) { 44 | Thread.sleep(700) 45 | _job1flow.value = counter / 100f 46 | counter += 5 47 | } 48 | Log.d(TAG, "Finished the loop") 49 | } 50 | } 51 | } 52 | 53 | 54 | fun setUpJobWithIsActiveCheck() { 55 | runBlocking { 56 | job = GlobalScope.launch( 57 | start = CoroutineStart.LAZY 58 | ) { 59 | _coroutineStatus.value = getCoroutineStatus(job) 60 | Log.d(TAG, "running cooperative cooroutine") 61 | var counter = 0 62 | while (counter <= 100 && isActive) { 63 | Thread.sleep(500) 64 | _job1flow.value = counter / 100f 65 | counter += 10 66 | } 67 | Log.d(TAG, "Finished the loop") 68 | } 69 | } 70 | } 71 | 72 | fun setUpJobWithCancellationException() { 73 | runBlocking { 74 | job = GlobalScope.launch( 75 | start = CoroutineStart.LAZY 76 | ) { 77 | _coroutineStatus.value = getCoroutineStatus(job) 78 | try { 79 | var counter = 0 80 | while (counter <= 100) { 81 | delay(500) 82 | _job1flow.value = counter / 100f 83 | counter += 10 84 | } 85 | } catch (e: CancellationException) { 86 | Log.d(TAG, "Caught cancellation exception") 87 | } 88 | } 89 | } 90 | } 91 | 92 | //https://lottiefiles.com/blog/working-with-lottie/getting-started-with-lottie-animations-in-android-app 93 | 94 | 95 | fun start() { 96 | job.start() 97 | } 98 | 99 | fun cancel() { 100 | viewModelScope.launch { 101 | job.cancel() 102 | _coroutineStatus.value = getCoroutineStatus(job) 103 | //give some time to see that the coroutine was cancelled 104 | delay(500) 105 | job.join() 106 | _coroutineStatus.value = getCoroutineStatus(job) 107 | Log.d(TAG, "Cancellation completed") 108 | } 109 | } 110 | 111 | private fun getCoroutineStatus(job: Job): String = with(job) { 112 | return when { 113 | isActive -> "Active" 114 | isCancelled && isCompleted -> "Completed" 115 | isCancelled -> "Cancelled" 116 | else -> "Not started" 117 | } 118 | } 119 | 120 | fun stopUncooperative() { 121 | shouldRun = false 122 | } 123 | 124 | fun clear() { 125 | _job1flow.value = 0f 126 | setupUncooperativeCoroutine() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/coroutine_scope/CoroutineScopeViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.coroutine_scope 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.coroutineScope 7 | import kotlinx.coroutines.delay 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.asStateFlow 10 | import kotlinx.coroutines.launch 11 | 12 | 13 | class CoroutineScopeViewModel : ViewModel() { 14 | 15 | private val _task1Flow = MutableStateFlow(Pair(1f, "Task 1 : 5.0s")) 16 | val task1Flow = _task1Flow.asStateFlow() 17 | 18 | private val _task2Flow = MutableStateFlow(Pair(1f, "Task 2 : 7.0s")) 19 | val task2Flow = _task2Flow.asStateFlow() 20 | 21 | private val _task3Flow = MutableStateFlow(Pair(1f, "Task 3 : 3.0s")) 22 | val task3Flow = _task3Flow.asStateFlow() 23 | 24 | private val _task4Flow = MutableStateFlow(Pair(1f, "Task 4: 1.0s")) 25 | val task4Flow = _task4Flow.asStateFlow() 26 | 27 | fun onCoroutineScopeClicked() { 28 | viewModelScope.launch { 29 | launch { 30 | sendTimeUpdatesInto(_task1Flow, 5000L, 1) 31 | println("Task 1") 32 | } 33 | 34 | coroutineScope { 35 | launch { 36 | sendTimeUpdatesInto(_task2Flow, 7000L, 2) 37 | println("Task 2") 38 | } 39 | sendTimeUpdatesInto(_task3Flow, 3000L, 3) 40 | println("Task 3") 41 | } 42 | 43 | sendTimeUpdatesInto(_task4Flow, 2000L, 4) 44 | println("Task 4") 45 | } 46 | } 47 | 48 | private suspend fun sendTimeUpdatesInto( 49 | flow: MutableStateFlow>, 50 | delay: Long, 51 | taskNo: Int = 0 52 | ) { 53 | for (i in (delay / 100).toInt() downTo 0) { 54 | Log.d( 55 | "CoroutineScopeViewModel", 56 | "sendTimeUpdatesInto: $${i.toFloat() / (delay / 100)} i = $i" 57 | ) 58 | flow.value = Pair(i.toFloat() / (delay / 100), "Task $taskNo : ${i / 10},${i % 10}s") 59 | delay(100) 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/deadlock/DeadLockScreen.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.Box 2 | import androidx.compose.foundation.layout.Column 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.material.Button 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.hilt.navigation.compose.hiltViewModel 11 | import nick.mirosh.androidsamples.ui.coroutines.deadlock.DeadLockViewModel 12 | 13 | 14 | @Composable 15 | fun DeadLockScreen( 16 | viewModel: DeadLockViewModel = hiltViewModel() 17 | ) { 18 | MaterialTheme { 19 | Box(modifier = Modifier.fillMaxWidth()) { 20 | Column(modifier = Modifier.align(Alignment.Center)) { 21 | Button(onClick = { 22 | viewModel.runLogicalDeadlock() 23 | }) { 24 | Text("Run Logical Deadlock") 25 | } 26 | 27 | Button(onClick = { 28 | viewModel.runActualDeadlock() 29 | }) { 30 | Text("Run Actual Deadlock. Warning: this will freeze the app.") 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/deadlock/DeadLockViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.deadlock 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Deferred 8 | import kotlinx.coroutines.DelicateCoroutinesApi 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.ExperimentalCoroutinesApi 11 | import kotlinx.coroutines.async 12 | import kotlinx.coroutines.awaitAll 13 | import kotlinx.coroutines.channels.Channel 14 | import kotlinx.coroutines.channels.actor 15 | import kotlinx.coroutines.coroutineScope 16 | import kotlinx.coroutines.delay 17 | import kotlinx.coroutines.flow.SharedFlow 18 | import kotlinx.coroutines.joinAll 19 | import kotlinx.coroutines.launch 20 | import kotlinx.coroutines.newSingleThreadContext 21 | import kotlinx.coroutines.runBlocking 22 | import kotlinx.coroutines.sync.Mutex 23 | import kotlinx.coroutines.sync.withLock 24 | import kotlinx.coroutines.withTimeout 25 | import java.util.concurrent.atomic.AtomicInteger 26 | import kotlin.system.measureTimeMillis 27 | 28 | private const val TAG = "DeadLockViewModel" 29 | 30 | class DeadLockViewModel : ViewModel() { 31 | fun runLogicalDeadlock() { 32 | viewModelScope.launch { 33 | try { 34 | withTimeout(5000L) { 35 | deadLock() 36 | } 37 | } catch (e: Exception) { 38 | Log.e(TAG, "Exception: ${e.message}") 39 | } 40 | } 41 | } 42 | fun runActualDeadlock() { 43 | runBlocking { 44 | deadLock() 45 | } 46 | } 47 | 48 | private suspend fun deadLock() { 49 | coroutineScope { 50 | var deferred2: Deferred? = null 51 | val deferred1 = async { 52 | deferred2?.await() 53 | "Result of task 1:" 54 | } 55 | deferred2 = async { 56 | deferred1.await() 57 | "Result of task 2" 58 | } 59 | deferred1.await() 60 | deferred2.await() 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/exceptions/different_exceptions/DifferentExceptionsViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.exceptions.different_exceptions 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import kotlinx.coroutines.CancellationException 6 | import kotlinx.coroutines.CoroutineExceptionHandler 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.asStateFlow 11 | import kotlinx.coroutines.joinAll 12 | import kotlinx.coroutines.launch 13 | import nick.mirosh.androidsamples.ui.coroutines.logStackTrace 14 | import nick.mirosh.androidsamples.ui.coroutines.runElapsingUpdates 15 | 16 | const val TAG = "DifferentExceptionsViewModel" 17 | 18 | class DifferentExceptionsViewModel : ViewModel() { 19 | 20 | private val _task1Flow = MutableStateFlow(ProgressUpdate(label = "12.0s")) 21 | val task1Flow = _task1Flow.asStateFlow() 22 | 23 | private val _task2Flow = MutableStateFlow(ProgressUpdate(label = "18.0s")) 24 | val task2Flow = _task2Flow.asStateFlow() 25 | 26 | private val _task3Flow = MutableStateFlow(ProgressUpdate(label = "5.0s")) 27 | val task3Flow = _task3Flow.asStateFlow() 28 | 29 | private val _firstCoroutineCancelled = MutableStateFlow(false) 30 | val firstCoroutineCancelled = _firstCoroutineCancelled.asStateFlow() 31 | 32 | private val _thirdCoroutineCancelled = MutableStateFlow(false) 33 | val thirdCoroutineCancelled = _thirdCoroutineCancelled.asStateFlow() 34 | 35 | fun simpleChallenge() { 36 | val exceptionHandler = CoroutineExceptionHandler { _, throwable -> 37 | throwable.logStackTrace(TAG) 38 | } 39 | //An exception handler has to be added if we don't want the application to crash 40 | CoroutineScope(Dispatchers.IO + exceptionHandler).launch { 41 | try { 42 | val job1 = launch { 43 | Log.d(TAG, "child1") 44 | runElapsingUpdates(_task1Flow, 12000L) 45 | Log.d(TAG, "child1 working") 46 | throw RuntimeException() 47 | } 48 | 49 | val job2 = launch { 50 | Log.d(TAG, "child2") 51 | runElapsingUpdates(_task2Flow, 18000L) 52 | Log.d(TAG, "child2 finishing") 53 | } 54 | val job3 = launch { 55 | runElapsingUpdates(_task3Flow, 5000L) 56 | _thirdCoroutineCancelled.emit(true) 57 | throw CancellationException() 58 | } 59 | joinAll(job1, job2, job3) 60 | } catch (e: Exception) { 61 | _firstCoroutineCancelled.emit(true) 62 | Log.d(TAG, "exception caught") 63 | } 64 | } 65 | } 66 | } 67 | 68 | 69 | data class ProgressUpdate( 70 | val progress: Float = 1f, 71 | val label: String = "", 72 | val failed: Boolean = false, 73 | ) -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/exceptions/exception_propagation/ExceptionPropagationScreen.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.background 2 | import androidx.compose.foundation.layout.Box 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.foundation.rememberScrollState 9 | import androidx.compose.foundation.verticalScroll 10 | import androidx.compose.material.Button 11 | import androidx.compose.material.Checkbox 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color.Companion.White 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | import androidx.hilt.navigation.compose.hiltViewModel 23 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 24 | import nick.mirosh.androidsamples.ui.coroutines.CancellableProgressBar 25 | import nick.mirosh.androidsamples.ui.coroutines.exceptions.exception_propagation.ExceptionPropagationViewModel 26 | import nick.mirosh.androidsamples.ui.coroutines.firstLevelIndent 27 | import nick.mirosh.androidsamples.ui.coroutines.secondLevelIndent 28 | 29 | 30 | @Composable 31 | fun ExceptionPropagationScreen( 32 | viewModel: ExceptionPropagationViewModel = hiltViewModel() 33 | ) { 34 | 35 | val grandParentProgress by viewModel.grandParentProgressUpdate.collectAsStateWithLifecycle() 36 | val parentProgress by viewModel.parentProgressUpdate.collectAsStateWithLifecycle() 37 | val childProgress by viewModel.childProgressUpdate.collectAsStateWithLifecycle() 38 | val scrollState = rememberScrollState() 39 | var cancelled by remember { mutableStateOf(false) } 40 | Column( 41 | modifier = Modifier 42 | .verticalScroll(scrollState) 43 | .padding(16.dp) 44 | ) { 45 | 46 | Button(onClick = { 47 | viewModel.simpleChallenge() 48 | }) { 49 | Text("start propagation") 50 | } 51 | Text( 52 | modifier = Modifier.padding(top = 16.dp), 53 | text = "GrandParent", 54 | ) 55 | Row( 56 | verticalAlignment = androidx.compose.ui.Alignment.CenterVertically 57 | ) { 58 | CancellableProgressBar( 59 | modifier = Modifier 60 | .width(200.dp), 61 | progress = grandParentProgress, 62 | cancelled = cancelled, 63 | ) 64 | TextCheckBox( 65 | checked = false, 66 | onCheckedChange = { 67 | cancelled = it 68 | } 69 | ) 70 | } 71 | Text( 72 | modifier = Modifier.padding(top = 16.dp), 73 | text = "Parent", 74 | ) 75 | CancellableProgressBar( 76 | modifier = Modifier 77 | .padding(top = 16.dp) 78 | .firstLevelIndent(), 79 | progress = parentProgress, 80 | cancelled = cancelled, 81 | ) 82 | Text( 83 | modifier = Modifier.padding(top = 16.dp), 84 | text = "Child", 85 | ) 86 | CancellableProgressBar( 87 | modifier = Modifier 88 | .padding(top = 16.dp) 89 | .secondLevelIndent(), 90 | progress = childProgress, 91 | cancelled = cancelled, 92 | ) 93 | } 94 | } 95 | 96 | @Composable 97 | fun TextCheckBox( 98 | checked: Boolean, 99 | onCheckedChange: (Boolean) -> Unit, 100 | ) { 101 | Row( 102 | verticalAlignment = androidx.compose.ui.Alignment.CenterVertically 103 | ) 104 | { 105 | Checkbox( 106 | checked = checked, 107 | onCheckedChange = { 108 | onCheckedChange(it) 109 | } 110 | ) 111 | Text( 112 | "Catch here" 113 | ) 114 | } 115 | } 116 | 117 | @Composable 118 | @Preview 119 | fun ExceptionPropagationScreenPreview() { 120 | Box( 121 | modifier = Modifier 122 | .background(color = White) 123 | .fillMaxSize() 124 | ) { 125 | ExceptionPropagationScreen() 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/exceptions/exception_propagation/ExceptionPropagationViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.exceptions.exception_propagation 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.CoroutineExceptionHandler 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.GlobalScope 10 | import kotlinx.coroutines.delay 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.joinAll 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.runBlocking 15 | import nick.mirosh.androidsamples.ui.coroutines.runUpdatesIn 16 | import kotlin.coroutines.cancellation.CancellationException 17 | 18 | const val TAG = "ExceptionPropagationViewModel" 19 | 20 | class ExceptionPropagationViewModel : ViewModel() { 21 | 22 | 23 | private val _childProgressUpdate = MutableStateFlow(0f) 24 | val childProgressUpdate = _childProgressUpdate 25 | 26 | private val _parentProgressUpdate = MutableStateFlow(0f) 27 | val parentProgressUpdate = _parentProgressUpdate 28 | 29 | private val _grandParentProgressUpdate = MutableStateFlow(0f) 30 | val grandParentProgressUpdate = _grandParentProgressUpdate 31 | 32 | fun start() { 33 | // https://medium.com/@manuaravindpta/exception-handling-in-kotlin-coroutine-34ef9bee3f8c#:~:text=The%20output%20is%20as%20follows.&text=We%20found%20that%20the%20exception,and%20can%20continue%20to%20execute. 34 | val handler = CoroutineExceptionHandler { _, exception -> 35 | Log.d( 36 | TAG, 37 | "catching final exception in GRAND GRAND parent coroutine with message: ${exception.message}" 38 | ) 39 | } 40 | viewModelScope 41 | .launch( 42 | Dispatchers.IO + handler 43 | ) { 44 | val grandParentJob = launch { 45 | try { 46 | val parentJob = launch { 47 | try { 48 | val childJob = launchChild() 49 | Log.d(TAG, "Waiting for the child coroutine to finish") 50 | childJob.join() 51 | } catch (e: Exception) { 52 | Log.d( 53 | TAG, 54 | "catching exception in parent coroutine with message: ${e.message}" 55 | ) 56 | } 57 | runUpdatesIn(_parentProgressUpdate) 58 | } 59 | parentJob.join() 60 | } catch (e: Exception) { 61 | Log.d( 62 | TAG, 63 | "catching exception in GRAND parent coroutine with message: ${e.message}" 64 | ) 65 | } 66 | runUpdatesIn(_grandParentProgressUpdate) 67 | } 68 | } 69 | } 70 | 71 | 72 | private fun CoroutineScope.launchChild() = launch { 73 | delay(2000) 74 | Log.d(TAG, "throwing exception in child coroutine") 75 | throw UnsupportedOperationException("Exception in child coroutine") 76 | } 77 | 78 | 79 | fun main() { 80 | runBlocking { 81 | val job = GlobalScope.launch { 82 | var counter = 0 83 | while (true) { 84 | Log.d(TAG, "Counter: $counter") 85 | counter++ 86 | Thread.sleep(100) 87 | } 88 | } 89 | delay(1000) 90 | Log.d(TAG, "Requesting cancellation") 91 | job.cancel() 92 | job.join() 93 | Log.d(TAG, "Cancellation requested") 94 | } 95 | } 96 | 97 | fun challenge() { 98 | try { 99 | runBlocking { 100 | val job = launch { 101 | launch { 102 | throw ArithmeticException("Division by zero") 103 | } 104 | launch { 105 | delay(100L) 106 | throw IndexOutOfBoundsException("Index error") 107 | } 108 | } 109 | 110 | // job.join() 111 | Log.d(TAG, "Completed runBlocking scope") 112 | } 113 | } catch (e: Exception) { 114 | Log.d(TAG, "outer catch with ${e.message}") 115 | } 116 | } 117 | 118 | 119 | fun simpleChallenge() { 120 | CoroutineScope(Dispatchers.IO).launch { 121 | launch { 122 | Log.d(TAG, "child1") 123 | delay(200) 124 | Log.d(TAG, "child1 working") 125 | throw RuntimeException() 126 | } 127 | 128 | launch { 129 | Log.d(TAG, "child2") 130 | delay(300) 131 | Log.d(TAG, "child2 finishing") 132 | } 133 | 134 | launch { 135 | delay(100) 136 | throw CancellationException() 137 | } 138 | } 139 | } 140 | 141 | fun challenge2() { 142 | try { 143 | runBlocking { 144 | val grandparentJob = launch { 145 | Log.d(TAG, "Grandparent is started") 146 | 147 | val parentJob = launch { 148 | val childJob = launch { 149 | Log.d(TAG, "child is started") 150 | delay(100) 151 | throw CancellationException() 152 | } 153 | 154 | val childJob2 = launch { 155 | Log.d(TAG, "child2 is started") 156 | delay(200) 157 | throw RuntimeException() 158 | } 159 | 160 | val childJob3 = launch { 161 | delay(300) 162 | Log.d(TAG, "child3 is started") 163 | } 164 | 165 | try { 166 | joinAll(childJob, childJob2, childJob3) 167 | } catch (e: Exception) { 168 | Log.d(TAG, "Caught in parent: $e with ${e.message}") 169 | } 170 | } 171 | try { 172 | parentJob.join() 173 | } catch (e: Exception) { 174 | Log.d(TAG, "Caught in grandparent: $e") 175 | } 176 | 177 | Log.d(TAG, "Grandparent is completing") 178 | } 179 | 180 | grandparentJob.join() 181 | Log.d(TAG, "Completed runBlocking scope") 182 | } 183 | } catch (e: Exception) { 184 | 185 | Log.d(TAG, "caught outside of runBlocking") 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/exceptions/lobby/CoroutineExceptionsLobbyScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.exceptions.lobby 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material.Text 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.dp 15 | import androidx.navigation.NavGraphBuilder 16 | import androidx.navigation.NavHostController 17 | import androidx.navigation.compose.NavHost 18 | import androidx.navigation.compose.composable 19 | import androidx.navigation.compose.rememberNavController 20 | import nick.mirosh.androidsamples.navigateSingleTopTo 21 | import nick.mirosh.androidsamples.ui.navigation.CoroutineExceptionsLobbyDestination 22 | import nick.mirosh.androidsamples.ui.navigation.DifferentExceptionsChallengeDestination 23 | import nick.mirosh.androidsamples.ui.navigation.ExceptionPropagationDestination 24 | import nick.mirosh.androidsamples.ui.coroutines.async.AsyncComparisonScreen 25 | import nick.mirosh.androidsamples.ui.coroutines.exceptions.different_exceptions.DifferentExceptionsScreen 26 | 27 | @Composable 28 | fun CoroutineExceptionsLobbyScreen() { 29 | val navController = rememberNavController() 30 | Surface( 31 | modifier = Modifier.fillMaxSize(), 32 | color = MaterialTheme.colorScheme.background 33 | ) { 34 | NavHost( 35 | navController = navController, 36 | startDestination = CoroutineExceptionsLobbyDestination.route, 37 | modifier = Modifier 38 | ) { 39 | setUpCoroutineExceptionsNavigation(navController) 40 | } 41 | } 42 | } 43 | 44 | @Composable 45 | fun ExceptionsLobbyContent( 46 | onDifferentExceptionClicked: (() -> Unit)? = null, 47 | onExceptionPropagationClicked: (() -> Unit)? = null, 48 | ) { 49 | val scrollState = rememberScrollState() 50 | 51 | Column(modifier = Modifier.verticalScroll(scrollState)) { 52 | Text( 53 | text = "Different exceptions", 54 | modifier = Modifier 55 | .clickable { 56 | onDifferentExceptionClicked?.invoke() 57 | } 58 | .padding(24.dp) 59 | ) 60 | Text( 61 | text = "Exception Propagation", 62 | modifier = Modifier 63 | .clickable { 64 | onExceptionPropagationClicked?.invoke() 65 | } 66 | .padding(24.dp) 67 | ) 68 | } 69 | } 70 | 71 | fun NavGraphBuilder.setUpCoroutineExceptionsNavigation(navController: NavHostController) { 72 | composable(route = CoroutineExceptionsLobbyDestination.route) { 73 | ExceptionsLobbyContent( 74 | onDifferentExceptionClicked = { 75 | navController.navigateSingleTopTo( 76 | DifferentExceptionsChallengeDestination.route 77 | ) 78 | }, 79 | onExceptionPropagationClicked = { 80 | navController.navigateSingleTopTo( 81 | ExceptionPropagationDestination.route 82 | ) 83 | } 84 | ) 85 | } 86 | 87 | composable(route = ExceptionPropagationDestination.route) { 88 | AsyncComparisonScreen() 89 | } 90 | 91 | composable(route = DifferentExceptionsChallengeDestination.route) { 92 | DifferentExceptionsScreen() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/shared_ui_elements/ProgressBar.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.shared_ui_elements 2 | 3 | import androidx.compose.animation.core.FastOutSlowInEasing 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material.LinearProgressIndicator 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.unit.dp 15 | 16 | @Composable 17 | fun ProgressBar( 18 | modifier: Modifier = Modifier, 19 | progress: Float, 20 | ) { 21 | 22 | val progressAnimDuration = 300 23 | val progressAnimation by animateFloatAsState( 24 | targetValue = progress, 25 | animationSpec = tween( 26 | durationMillis = progressAnimDuration, 27 | easing = FastOutSlowInEasing 28 | ), 29 | label = "" 30 | ) 31 | LinearProgressIndicator( 32 | progress = progressAnimation, 33 | modifier = modifier 34 | .padding(horizontal = 16.dp) 35 | .height(32.dp) 36 | .clip(RoundedCornerShape(16.dp)) 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/shared_ui_elements/ProgressBarWithCancel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.shared_ui_elements 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.Button 10 | import androidx.compose.material.ButtonDefaults 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | 23 | @Composable 24 | fun ProgressBarWithCancel( 25 | modifier: Modifier = Modifier, 26 | progress: Float, 27 | label: String, 28 | onCancelClick: () -> Unit, 29 | ) { 30 | 31 | var cancelled by remember { 32 | mutableStateOf(false) 33 | } 34 | Column { 35 | Spacer(modifier = Modifier.height(16.dp)) 36 | Text( 37 | modifier = Modifier 38 | .padding(horizontal = 16.dp), 39 | text = label, 40 | fontSize = 20.sp 41 | ) 42 | Row( 43 | modifier = Modifier.fillMaxWidth(), 44 | verticalAlignment = Alignment.CenterVertically 45 | ) { 46 | ProgressBar( 47 | modifier = modifier 48 | .weight(2f / 3f), 49 | progress = progress 50 | ) 51 | 52 | Button( 53 | colors = if (cancelled) ButtonDefaults.buttonColors( 54 | backgroundColor = Color.Red, 55 | contentColor = Color.White 56 | ) 57 | else ButtonDefaults.buttonColors(), 58 | modifier = Modifier 59 | .weight(1f / 3f) 60 | .padding(horizontal = 16.dp), 61 | onClick = { 62 | cancelled = true 63 | onCancelClick() 64 | }) { 65 | Text(if (cancelled) "Cancelled" else "Cancel") 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/coroutines/synchronization/RaceConditionViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.coroutines.synchronization 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Deferred 8 | import kotlinx.coroutines.DelicateCoroutinesApi 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import kotlinx.coroutines.async 11 | import kotlinx.coroutines.awaitAll 12 | import kotlinx.coroutines.channels.Channel 13 | import kotlinx.coroutines.channels.actor 14 | import kotlinx.coroutines.launch 15 | import kotlinx.coroutines.newSingleThreadContext 16 | import kotlinx.coroutines.runBlocking 17 | import kotlinx.coroutines.sync.Mutex 18 | import kotlinx.coroutines.sync.withLock 19 | import nick.mirosh.androidsamples.ui.coroutines.cooperative_coroutine.TAG 20 | 21 | class RaceConditionViewModel: ViewModel() { 22 | 23 | // Shared mutable state 24 | //https://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html 25 | //threads can cache values they work with in their own local memory, such as CPU registers 26 | // or caches, for performance reasons. This caching mechanism can lead to a situation where the 27 | // value of a variable updated by one thread is not immediately visible to other threads, 28 | // because each thread may be working with a cached copy of the variable stored in its own local 29 | // memory. The result is a lack of consistency in the observed value of the variable 30 | // across different threads. 31 | 32 | // Accessing a volatile variable is more expensive than accessing a non-volatile variable 33 | // but less expensive than executing a synchronized block. Therefore, 34 | // it can be a good option when reading and writing a variable atomically without locking. 35 | @Volatile 36 | var counter = 0 37 | 38 | // var counter = AtomicInteger() 39 | @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) 40 | fun runRaceCondition() { 41 | viewModelScope.launch { 42 | val listOfJobs = mutableListOf>() 43 | repeat(10) { 44 | listOfJobs.add(async(newSingleThreadContext("CounterThread$it")) { 45 | incrementCounter() 46 | }) 47 | } 48 | listOfJobs.awaitAll() 49 | 50 | Log.d(TAG, "Expected value: 10000") 51 | Log.d(TAG, "Actual value: $counter") 52 | } 53 | } 54 | 55 | private fun incrementCounter() { 56 | Log.d(TAG, "thread: ${Thread.currentThread().name}") 57 | repeat(1000) { 58 | counter += 1 59 | } 60 | } 61 | 62 | val mutex = Mutex() 63 | private suspend fun incrementCounterMutex() { 64 | Log.d(TAG, "thread: ${Thread.currentThread().name}") 65 | repeat(1000) { 66 | mutex.withLock { 67 | counter += 1 68 | } 69 | } 70 | } 71 | 72 | val lock = Any() 73 | private suspend fun incrementCounterSynchronizedOnObject() { 74 | Log.d(TAG, "thread: ${Thread.currentThread().name}") 75 | repeat(1000) { 76 | synchronized(lock) { 77 | counter += 1 78 | } 79 | } 80 | } 81 | 82 | private fun incrementCounterSynchronizedOnThis() { 83 | Log.d(TAG, "thread: ${Thread.currentThread().name}") 84 | repeat(1000) { 85 | synchronized(this) { 86 | counter += 1 87 | } 88 | } 89 | } 90 | 91 | //TODO leaern what is actor 92 | fun CoroutineScope.counterActor() = actor { 93 | var count = 0 94 | for (msg in channel) { 95 | count += msg 96 | } 97 | } 98 | 99 | //TODO learn channel 100 | val channel = Channel() 101 | 102 | fun learnChannel() = runBlocking { 103 | // Coroutine 1 104 | launch { 105 | for (x in 1..5) channel.send(x) 106 | } 107 | 108 | // Coroutine 2 109 | launch { 110 | repeat(5) { println(channel.receive()) } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/dialog/WebViewDialog.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.dialog 2 | 3 | import android.webkit.WebView 4 | import android.webkit.WebViewClient 5 | import androidx.compose.foundation.layout.fillMaxHeight 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material.Card 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.viewinterop.AndroidView 14 | import androidx.compose.ui.window.Dialog 15 | import androidx.compose.ui.window.DialogProperties 16 | 17 | @Composable 18 | fun WebViewDialog( 19 | url: String, 20 | onDismissRequest: () -> Unit, 21 | ) { 22 | Dialog( 23 | onDismissRequest = onDismissRequest, 24 | properties = DialogProperties( 25 | usePlatformDefaultWidth = false 26 | ) 27 | ) { 28 | Card( 29 | modifier = Modifier 30 | .fillMaxWidth(fraction = 0.8f) 31 | .fillMaxHeight(fraction = 0.8f), 32 | shape = RoundedCornerShape(16.dp) 33 | ) { 34 | AndroidView( 35 | factory = { context -> 36 | WebView(context).apply { 37 | webViewClient = WebViewClient() 38 | settings.javaScriptEnabled = true 39 | loadUrl(url) 40 | } 41 | }, 42 | modifier = Modifier 43 | .fillMaxWidth() 44 | .height(400.dp) 45 | ) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/animation/AnimationContent.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.animation 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.geometry.Offset 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.StrokeCap 21 | import androidx.compose.ui.graphics.drawscope.Stroke 22 | import androidx.compose.ui.platform.LocalDensity 23 | import androidx.compose.ui.unit.dp 24 | import kotlinx.coroutines.delay 25 | 26 | const val ANIMATION_DURATION = 800 27 | 28 | enum class AnimationState { 29 | FIRST_IN_PROGRESS, SECOND_IN_PROGRESS, INITIAL 30 | } 31 | 32 | @Composable 33 | fun SmileyLoadingAnimation() { 34 | var animationState by remember { 35 | mutableStateOf(AnimationState.INITIAL) 36 | } 37 | LaunchedEffect(Unit) { 38 | while (true) { 39 | animationState = AnimationState.FIRST_IN_PROGRESS 40 | delay(ANIMATION_DURATION.toLong()) 41 | animationState = AnimationState.SECOND_IN_PROGRESS 42 | delay(ANIMATION_DURATION.toLong()) 43 | } 44 | } 45 | SmileyDrawer(animationState) 46 | } 47 | 48 | @Composable 49 | fun SmileyDrawer(animationState: AnimationState) { 50 | val widthAndHeight = 200.dp 51 | val density = LocalDensity.current 52 | val widthInPixels = with(density) { widthAndHeight.toPx() } 53 | val oneThird = widthInPixels / 3 54 | val twoThirds = widthInPixels / 3 * 2 55 | val circleRadius = 30f 56 | val animationColor = Color.Blue 57 | 58 | val animatedX: Float by animateFloatAsState( 59 | targetValue = if (animationState == AnimationState.SECOND_IN_PROGRESS) twoThirds else oneThird, 60 | animationSpec = tween(durationMillis = ANIMATION_DURATION), label = "" 61 | ) 62 | val animatedX2: Float by animateFloatAsState( 63 | targetValue = if (animationState == AnimationState.SECOND_IN_PROGRESS) widthInPixels else twoThirds, 64 | animationSpec = tween(durationMillis = ANIMATION_DURATION), label = "" 65 | ) 66 | val animatedX3: Float by animateFloatAsState( 67 | targetValue = if (animationState == AnimationState.SECOND_IN_PROGRESS) oneThird else 0f, 68 | animationSpec = tween(durationMillis = ANIMATION_DURATION), label = "" 69 | ) 70 | val sweepAngle by animateFloatAsState( 71 | targetValue = if (animationState == AnimationState.FIRST_IN_PROGRESS) 180f else 0f, 72 | animationSpec = tween(durationMillis = ANIMATION_DURATION), 73 | label = "", 74 | ) 75 | val sweepAngle2 by animateFloatAsState( 76 | targetValue = if (animationState == AnimationState.SECOND_IN_PROGRESS) 0f else -180f, 77 | animationSpec = tween(durationMillis = ANIMATION_DURATION), 78 | label = "", 79 | ) 80 | 81 | Box( 82 | modifier = Modifier 83 | .fillMaxSize() 84 | ) { 85 | Canvas( 86 | modifier = Modifier 87 | .height(widthAndHeight) 88 | .width(widthAndHeight) 89 | .align(Alignment.Center) 90 | ) { 91 | val circleY = size.height / 2 92 | if (animationState == AnimationState.FIRST_IN_PROGRESS) { 93 | drawCircle( 94 | radius = circleRadius, 95 | color = animationColor, 96 | center = Offset(widthInPixels, circleY) 97 | ) 98 | drawCircle( 99 | radius = circleRadius, 100 | color = animationColor, 101 | center = Offset(oneThird, circleY) 102 | ) 103 | drawCircle( 104 | radius = circleRadius, 105 | color = animationColor, 106 | center = Offset(twoThirds, circleY) 107 | ) 108 | drawArc( 109 | color = animationColor, 110 | topLeft = Offset(0f, 0f), 111 | startAngle = 0f, 112 | sweepAngle = sweepAngle, 113 | useCenter = false, 114 | style = Stroke( 115 | width = circleRadius * 2, 116 | cap = StrokeCap.Round 117 | ) 118 | ) 119 | } 120 | else { 121 | val arcCenter = 122 | Offset(animatedX3, 0f) 123 | drawCircle( 124 | radius = circleRadius, 125 | color = animationColor, 126 | center = Offset(animatedX, circleY) 127 | ) 128 | drawCircle( 129 | radius = circleRadius, 130 | color = animationColor, 131 | center = Offset(animatedX2, circleY) 132 | ) 133 | drawArc( 134 | color = animationColor, 135 | topLeft = arcCenter, 136 | startAngle = 180f, 137 | sweepAngle = sweepAngle2, 138 | useCenter = false, 139 | style = Stroke( 140 | width = circleRadius * 2, 141 | cap = StrokeCap.Round 142 | ) 143 | ) 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/bottom_nav/BottomBarScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.bottom_nav 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Favorite 5 | import androidx.compose.material.icons.filled.Home 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | 8 | sealed class BottomBarScreen(var title: String, var icon: ImageVector, var route: String) { 9 | 10 | object Home : BottomBarScreen("Home", Icons.Default.Home, "home") 11 | object Favorites : BottomBarScreen("Favorites", Icons.Default.Favorite, "favorites") 12 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/bottom_nav/BottomNavGraph.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.bottom_nav 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.NavHostController 5 | import androidx.navigation.compose.NavHost 6 | import androidx.navigation.compose.composable 7 | 8 | @Composable 9 | fun BottomNavGraph(navController: NavHostController) { 10 | NavHost(navController = navController, startDestination = BottomBarScreen.Home.route) { 11 | composable(route = BottomBarScreen.Home.route) { 12 | HomeScreen() 13 | } 14 | composable(route = BottomBarScreen.Favorites.route) { 15 | FavoritesScreen() 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/bottom_nav/BottomNavigationScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.bottom_nav 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.material.BottomNavigation 6 | import androidx.compose.material.BottomNavigationItem 7 | import androidx.compose.material.ContentAlpha 8 | import androidx.compose.material.LocalContentColor 9 | import androidx.compose.material.Text 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.Scaffold 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.navigation.NavController 15 | import androidx.navigation.NavDestination 16 | import androidx.navigation.NavDestination.Companion.hierarchy 17 | import androidx.navigation.compose.currentBackStackEntryAsState 18 | import androidx.navigation.compose.rememberNavController 19 | 20 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") 21 | @Composable 22 | fun BottomNavigationScreen() { 23 | val navController = rememberNavController() 24 | Scaffold(bottomBar = { BottomBar(navController = navController) }) { 25 | BottomNavGraph(navController = navController) 26 | } 27 | } 28 | 29 | 30 | @Composable 31 | fun BottomBar(navController: NavController) { 32 | val screens = listOf( 33 | BottomBarScreen.Home, 34 | BottomBarScreen.Favorites 35 | ) 36 | val backStackEntry by navController.currentBackStackEntryAsState() 37 | val currentDestination = backStackEntry?.destination 38 | 39 | BottomNavigation { 40 | screens.forEach { 41 | AddItem( 42 | screen = it, 43 | currentDestination = currentDestination, 44 | navController = navController 45 | ) 46 | } 47 | } 48 | } 49 | 50 | 51 | @Composable 52 | fun RowScope.AddItem( 53 | screen: BottomBarScreen, 54 | currentDestination: NavDestination?, 55 | navController: NavController 56 | ) { 57 | BottomNavigationItem( 58 | label = { 59 | Text(text = screen.title) 60 | }, 61 | selected = currentDestination?.hierarchy?.any { 62 | it.route == screen.route 63 | } == true, 64 | onClick = { navController.navigate(screen.route) }, 65 | 66 | unselectedContentColor = LocalContentColor.current.copy(alpha = ContentAlpha.disabled), 67 | icon = { 68 | Icon( 69 | imageVector = screen.icon, 70 | contentDescription = "Navigation Icon" 71 | ) 72 | }) 73 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/bottom_nav/FavoritesScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.bottom_nav 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material.Text 5 | import androidx.compose.runtime.Composable 6 | 7 | @Composable 8 | fun FavoritesScreen( 9 | ) { 10 | Box { 11 | Text(text = "Favorites") 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/bottom_nav/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.bottom_nav 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material.Text 5 | import androidx.compose.runtime.Composable 6 | 7 | @Composable 8 | fun HomeScreen( 9 | ) { 10 | Box { 11 | Text(text = "Home") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/measuring/MeasuringComposable.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.measuring 2 | 3 | import android.util.Log 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableFloatStateOf 12 | import androidx.compose.runtime.mutableIntStateOf 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.layout.onGloballyPositioned 19 | import androidx.compose.ui.layout.positionInRoot 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import kotlin.math.ceil 22 | 23 | private const val TAG = "MeasuringComposable" 24 | 25 | @Composable 26 | fun MeasuringComposable( 27 | passedInText: String 28 | ) { 29 | var text by remember { mutableStateOf(passedInText) } 30 | 31 | var textX by remember { mutableFloatStateOf(0f) } 32 | var buttonX by remember { mutableFloatStateOf(0f) } 33 | var widthOfText by remember { mutableIntStateOf(0) } 34 | LaunchedEffect( 35 | buttonX 36 | ) { 37 | val shouldCut = buttonX - textX < widthOfText 38 | 39 | Log.d( 40 | TAG, "buttonX = $buttonX" + 41 | "\n textX = $textX" + 42 | "\n widthOfText = $widthOfText" 43 | ) 44 | if (shouldCut) { 45 | val avCharWidth = (widthOfText / text.length) 46 | val spaceToCut = widthOfText - (buttonX - textX) 47 | val charToCut = ceil(spaceToCut / avCharWidth).toInt() + 2 48 | text = "${text.substring(0, text.length - charToCut)}.." 49 | Log.d( 50 | TAG, 51 | "buttonX: $buttonX," + 52 | "\n textX: $textX," + 53 | "\n widthOfText: $widthOfText, " + 54 | "\n avCharWidth: $avCharWidth, " + 55 | "\n spaceToCut: $spaceToCut, " + 56 | "\n charToCut: $charToCut" 57 | ) 58 | } 59 | } 60 | Box( 61 | modifier = Modifier.fillMaxWidth() 62 | ) { 63 | 64 | Button( 65 | modifier = Modifier 66 | .align(Alignment.CenterEnd) 67 | .onGloballyPositioned { 68 | buttonX = it.positionInRoot().x 69 | }, 70 | onClick = { 71 | text += " Word" 72 | } 73 | ) { 74 | Text("Increase text size") 75 | } 76 | Text( 77 | modifier = Modifier 78 | .align(Alignment.CenterStart) 79 | .onGloballyPositioned { 80 | textX = it.positionInRoot().x 81 | widthOfText = it.size.width 82 | }, 83 | text = text 84 | ) 85 | // val density: Density = LocalDensity.current 86 | // if (buttonX != 0f) { 87 | // Text( 88 | // "X", 89 | // modifier = Modifier 90 | // .offset(x = with(density) { 91 | // buttonX.toDp() 92 | // }, y = 20.dp), 93 | // style = TextStyle(fontSize = 20.sp, color = androidx.compose.ui.graphics.Color.Red) 94 | // ) 95 | // } 96 | } 97 | } 98 | 99 | @Preview( 100 | showBackground = true, 101 | ) 102 | @Composable 103 | fun MeasuringComposablePreview() { 104 | MeasuringComposable("balls") 105 | } 106 | 107 | @Preview( 108 | showBackground = true, 109 | ) 110 | @Composable 111 | fun MeasuringComposablePreview2() { 112 | MeasuringComposable("balls asdfkasljd;faskjdf;laskdjf fjdkfjdksfjj fjdkfjskdfj dkjfjskdfj") 113 | } 114 | 115 | @Preview( 116 | showBackground = true, 117 | ) 118 | @Composable 119 | fun MeasuringComposablePreview3() { 120 | MeasuringComposable("balls asdfkasljd;faskjdf;fjdkfjdksfjj fjdkfjskdfj dkjfjskdfj") 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/parallax/CollapsibleHeaderScreen.kt: -------------------------------------------------------------------------------- 1 | import android.annotation.SuppressLint 2 | import androidx.compose.animation.animateContentSize 3 | import androidx.compose.animation.core.animateDpAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.LazyListState 9 | import androidx.compose.foundation.lazy.items 10 | import androidx.compose.foundation.lazy.rememberLazyListState 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Scaffold 13 | import androidx.compose.material.Text 14 | import androidx.compose.material.TopAppBar 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | 24 | @SuppressLint("UnusedMaterialScaffoldPaddingParameter") 25 | @Composable 26 | fun CollapsibleHeaderScreen() { 27 | val lazyListState = rememberLazyListState() 28 | Scaffold( 29 | content = { 30 | Box(modifier = Modifier.fillMaxSize()) { 31 | MainContent(lazyListState = lazyListState) 32 | TopBar(lazyListState = lazyListState) 33 | } 34 | } 35 | ) 36 | } 37 | 38 | @Composable 39 | fun TopBar(lazyListState: LazyListState) { 40 | TopAppBar( 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | .background(color = MaterialTheme.colors.primary) 44 | .animateContentSize(animationSpec = tween(durationMillis = 300)) 45 | .height(height = if (lazyListState.isScrolled) 0.dp else TOP_BAR_HEIGHT), 46 | contentPadding = PaddingValues(start = 16.dp) 47 | ) { 48 | Text( 49 | text = "Title", 50 | style = TextStyle( 51 | fontSize = MaterialTheme.typography.h6.fontSize, 52 | color = MaterialTheme.colors.surface 53 | ) 54 | ) 55 | } 56 | } 57 | 58 | @Composable 59 | fun MainContent(lazyListState: LazyListState) { 60 | val numbers = remember { List(size = 25) { it } } 61 | val padding by animateDpAsState( 62 | targetValue = if (lazyListState.isScrolled) 0.dp else TOP_BAR_HEIGHT, 63 | animationSpec = tween(durationMillis = 300) 64 | ) 65 | LazyColumn( 66 | modifier = Modifier.padding(top = padding), 67 | state = lazyListState 68 | ) { 69 | items( 70 | items = numbers, 71 | key = { it } 72 | ) { 73 | NumberHolder(number = it) 74 | } 75 | } 76 | } 77 | 78 | @Composable 79 | fun NumberHolder(number: Int) { 80 | Row( 81 | modifier = Modifier.fillMaxWidth(), 82 | verticalAlignment = Alignment.CenterVertically 83 | ) { 84 | Text( 85 | text = number.toString(), 86 | style = TextStyle( 87 | fontSize = MaterialTheme.typography.h1.fontSize, 88 | fontWeight = FontWeight.Bold 89 | ) 90 | ) 91 | } 92 | } 93 | 94 | val TOP_BAR_HEIGHT = 56.dp 95 | val LazyListState.isScrolled: Boolean 96 | get() = firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0 97 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/parallax/ParallaxColumnRunner.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.parallax 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.unit.dp 15 | import nick.mirosh.androidsamples.R 16 | import nick.mirosh.parallaxcolumn.ParallaxColumn 17 | import nick.mirosh.parallaxcolumn.PictureUri 18 | import nick.mirosh.parallaxcolumn.UriParallaxColumn 19 | 20 | 21 | @Composable 22 | fun UriParallaxColumnRunner() { 23 | 24 | val localUris = listOf( 25 | R.raw.amine_msiouri, 26 | R.raw.connor_danylenko, 27 | R.raw.julia_volk, 28 | R.raw.lukas_dlutko 29 | ).map { 30 | PictureUri.RawResource(it) 31 | } 32 | 33 | val authors = listOf( 34 | "Amine Msiouri", 35 | "Connor Danylenko", 36 | "Julia Volk", 37 | "Lukas Dlutko" 38 | ) 39 | 40 | UriParallaxColumn(pictureUris = localUris) { 41 | 42 | Box( 43 | modifier = Modifier 44 | .align(Alignment.BottomStart) 45 | .padding(8.dp), 46 | ) { 47 | Text( 48 | text = authors[it], 49 | modifier = Modifier 50 | .background(Color.White) 51 | .clip(RoundedCornerShape(4.dp)) // Rounded corners 52 | ) 53 | } 54 | } 55 | } 56 | 57 | 58 | @Composable 59 | fun ParallaxColumnRunner(bitmaps: List) { 60 | val authors = listOf( 61 | "Amine Msiouri", 62 | "Connor Danylenko", 63 | "Julia Volk", 64 | "Lukas Dlutko" 65 | ) 66 | 67 | ParallaxColumn(bitmaps = bitmaps) { 68 | Box( 69 | modifier = Modifier 70 | .align(Alignment.BottomStart) 71 | .padding(8.dp), 72 | ) { 73 | Text( 74 | text = authors[it], 75 | modifier = Modifier 76 | .background(Color.White) 77 | .clip(RoundedCornerShape(4.dp)) // Rounded corners 78 | ) 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/produce_state/ProduceStateScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.produce_state 2 | 3 | import android.util.Log 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.material.Button 7 | import androidx.compose.material.CircularProgressIndicator 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.runtime.collectAsState 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableIntStateOf 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.produceState 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.hilt.navigation.compose.hiltViewModel 21 | import kotlinx.coroutines.delay 22 | import kotlinx.coroutines.flow.Flow 23 | import kotlinx.coroutines.flow.flow 24 | import nick.mirosh.androidsamples.ui.jetpack_compose.side_effects.disposable_effect.UserProfile 25 | 26 | private const val TAG = "ProduceStateScreen" 27 | 28 | @Composable 29 | fun ProduceStateScreen() { 30 | example2Runner() 31 | } 32 | 33 | @Composable 34 | fun UserProfileUsingProduceState() { 35 | val userProfile by produceState( 36 | initialValue = null 37 | ) { 38 | value = getUserProfile() 39 | } 40 | Box { 41 | when (userProfile) { 42 | null -> CircularProgressIndicator() 43 | else -> Text(userProfile!!.name) 44 | } 45 | } 46 | } 47 | 48 | 49 | @Composable 50 | fun UserProfileUsingLaunchedEffect(userId: String) { 51 | var userProfile by remember { 52 | mutableStateOf(null) 53 | } 54 | var isLoading by remember { mutableStateOf(true) } 55 | 56 | LaunchedEffect(userId) { 57 | val profile = getUserProfile() 58 | isLoading = false 59 | userProfile = profile 60 | } 61 | 62 | when { 63 | isLoading -> CircularProgressIndicator() 64 | userProfile != null -> Text(userProfile!!.name) 65 | } 66 | } 67 | 68 | 69 | @Composable 70 | fun UserProfileUsingViewModel( 71 | viewModel: UserProfileViewModel = hiltViewModel() 72 | ) { 73 | val userProfile by viewModel.userProfile 74 | .collectAsState(initial = null) 75 | 76 | when (userProfile) { 77 | null -> CircularProgressIndicator() 78 | else -> Text(userProfile!!.name) 79 | } 80 | } 81 | 82 | @Composable 83 | fun UserProfileUsingProduceState2() { 84 | val userProfile by produceState(initialValue = null) { 85 | value = getUserProfile() 86 | } 87 | Box { 88 | when (val profile = userProfile) { 89 | null -> CircularProgressIndicator( 90 | modifier = Modifier.align(Alignment.Center) 91 | ) 92 | 93 | else -> Text( 94 | modifier = Modifier.align(Alignment.Center), 95 | text = profile.name 96 | ) 97 | } 98 | } 99 | } 100 | 101 | @Composable 102 | fun UserProfileUsingLaunchedEffect2(userId: String) { 103 | var userProfile by remember { mutableStateOf(null) } 104 | var isLoading by remember { mutableStateOf(true) } 105 | 106 | LaunchedEffect(userId) { 107 | val profile = getUserProfile() 108 | isLoading = false 109 | userProfile = profile 110 | } 111 | 112 | Box { 113 | when { 114 | isLoading -> CircularProgressIndicator() 115 | userProfile != null -> Text( 116 | modifier = Modifier.align(Alignment.Center), 117 | text = userProfile!!.name 118 | ) 119 | } 120 | } 121 | } 122 | 123 | @Composable 124 | fun UserProfileUsingViewModel2(viewModel: UserProfileViewModel = hiltViewModel()) { 125 | val userProfile by viewModel.userProfile.collectAsState(initial = null) 126 | Box { 127 | when (userProfile) { 128 | null -> CircularProgressIndicator() 129 | else -> Text( 130 | modifier = Modifier.align(Alignment.Center), 131 | text = userProfile!!.name 132 | ) 133 | } 134 | } 135 | } 136 | 137 | private suspend fun getUserProfile(): UserProfile { 138 | delay(1000) 139 | return UserProfile("Nick Miroshnychenko") 140 | } 141 | 142 | 143 | /////////////// /////////////// /////////////// /////////////// 144 | fun fetchDataPeriodically(tag: String): Flow = flow { 145 | for (i in 1..5) { 146 | // Simulate network request 147 | 148 | delay(2000) // Delay of 2 seconds 149 | emit("Data fetch $i at ${System.currentTimeMillis()}") 150 | Log.d(TAG, "$tag Data fetch $i at ${System.currentTimeMillis()}") 151 | } 152 | } 153 | 154 | 155 | @Composable 156 | fun example2Runner() { 157 | 158 | var count by remember { mutableIntStateOf(0) } 159 | 160 | Box { 161 | 162 | Button( 163 | modifier = Modifier.align(Alignment.Center), 164 | onClick = { 165 | count++ 166 | } 167 | ) { 168 | Text(text = "Click me: $count") 169 | } 170 | Column { 171 | if (count > 0) { 172 | DataDisplayUsingProduceState(count) 173 | // DataDisplayUsingLaunchedEffect(count) 174 | } 175 | // if (count == 1) { 176 | // DataDisplayUsingProduceState("2") 177 | // DataDisplayUsingLaunchedEffect("2") 178 | // } 179 | } 180 | } 181 | } 182 | 183 | @Composable 184 | fun DataDisplayUsingProduceState(tag: Int) { 185 | Log.d(TAG, "DataDisplayUsingProduceState: ") 186 | val data by produceState(initialValue = null) { 187 | fetchDataPeriodically("Produce state $tag").collect { newData -> 188 | value = newData 189 | } 190 | } 191 | 192 | Text(data ?: "Loading...") 193 | } 194 | 195 | @Composable 196 | fun DataDisplayUsingLaunchedEffect(tag: Int) { 197 | 198 | Log.d(TAG, " DataDisplayUsingLaunchedEffect: ") 199 | var data by remember { mutableStateOf(null) } 200 | 201 | LaunchedEffect(Unit) { 202 | fetchDataPeriodically("Launched Effect $tag").collect { newData -> 203 | data = newData 204 | } 205 | } 206 | 207 | Text(data ?: "Loading...") 208 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/produce_state/UserProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.produce_state 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.delay 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flow 8 | import kotlinx.coroutines.flow.flowOn 9 | import nick.mirosh.androidsamples.ui.jetpack_compose.side_effects.disposable_effect.UserProfile 10 | 11 | class UserProfileViewModel : ViewModel() { 12 | val userProfile: Flow = flow { 13 | emit(getUserProfile()) 14 | }.flowOn(Dispatchers.IO) 15 | } 16 | 17 | private suspend fun getUserProfile(): UserProfile { 18 | delay(1000) 19 | return UserProfile("Nick Miroshnychenko") 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/progress/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/progress/AnimatedCounter.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.progress 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.animation.AnimatedContent 5 | import androidx.compose.animation.ExperimentalAnimationApi 6 | import androidx.compose.animation.slideInVertically 7 | import androidx.compose.animation.slideOutVertically 8 | import androidx.compose.animation.togetherWith 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.material.MaterialTheme 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.SideEffect 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableIntStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.text.TextStyle 20 | 21 | @OptIn(ExperimentalAnimationApi::class) 22 | @SuppressLint("UnusedContentLambdaTargetStateParameter") 23 | @Composable 24 | fun AnimatedCounter( 25 | count: Int, 26 | modifier: Modifier = Modifier, 27 | style: TextStyle = MaterialTheme.typography.h1 28 | ) { 29 | var oldCount by remember { 30 | mutableIntStateOf(count) 31 | } 32 | SideEffect { 33 | oldCount = count 34 | } 35 | 36 | Row(modifier = Modifier) { 37 | val countString = count.toString() 38 | val oldCountString = oldCount.toString() 39 | for (i in countString.indices) { 40 | val oldChar = oldCountString.getOrNull(i); 41 | val newChar = countString[i] 42 | val char = if (oldChar == newChar) { 43 | oldCountString[i] 44 | } else { 45 | countString[i] 46 | } 47 | AnimatedContent( 48 | targetState = char, 49 | label = "", 50 | transitionSpec = { 51 | slideInVertically { it } togetherWith slideOutVertically { -it } 52 | } 53 | ) { char -> 54 | Text( 55 | text = char.toString(), 56 | style = style, 57 | softWrap = false 58 | ) 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/progress/ProgressBarContent.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.progress 2 | 3 | import androidx.compose.animation.core.FastOutSlowInEasing 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.width 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material3.Button 18 | import androidx.compose.material3.CircularProgressIndicator 19 | import androidx.compose.material3.LinearProgressIndicator 20 | import androidx.compose.material3.Switch 21 | import androidx.compose.material3.Text 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.collectAsState 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.mutableIntStateOf 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.draw.clip 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.unit.sp 34 | 35 | @Composable 36 | fun ProgressBarContent2( 37 | viewModel: ProgressBarViewModel, 38 | modifier: Modifier = Modifier 39 | ) { 40 | var isLinearProgress by remember { mutableStateOf(false) } 41 | val indicatorProgress by viewModel.progress.collectAsState() 42 | val progressAnimDuration = 300 43 | val progressAnimation by animateFloatAsState( 44 | targetValue = indicatorProgress.toFloat() / 100f, 45 | animationSpec = tween( 46 | durationMillis = progressAnimDuration, 47 | easing = FastOutSlowInEasing 48 | ), 49 | label = "" 50 | ) 51 | Box(modifier = Modifier.fillMaxSize()) { 52 | Column( 53 | modifier = Modifier 54 | .align(Alignment.Center), 55 | verticalArrangement = Arrangement.spacedBy(16.dp), 56 | horizontalAlignment = Alignment.CenterHorizontally 57 | ) { 58 | AnimatedCounter(count = indicatorProgress) 59 | Text( 60 | text = "Start", 61 | fontSize = 30.sp, 62 | modifier = Modifier 63 | .clickable { 64 | viewModel.onStartPressed() 65 | } 66 | .padding(24.dp) 67 | ) 68 | Spacer(modifier = Modifier.height(16.dp)) 69 | Switch( 70 | checked = isLinearProgress, 71 | onCheckedChange = { isLinearProgress = !isLinearProgress } 72 | ) 73 | if (isLinearProgress) { 74 | LinearProgressIndicator( 75 | progress = progressAnimation, 76 | modifier = Modifier 77 | .padding(horizontal = 16.dp) 78 | .fillMaxWidth() 79 | .height(32.dp) 80 | .clip(RoundedCornerShape(16.dp)) 81 | ) 82 | } else { 83 | CircularProgressIndicator( 84 | progress = progressAnimation, 85 | strokeWidth = 16.dp, 86 | trackColor = androidx.compose.ui.graphics.Color.LightGray, 87 | modifier = Modifier 88 | .width(150.dp) 89 | .height(150.dp) 90 | ) 91 | } 92 | } 93 | } 94 | } 95 | 96 | @Composable 97 | fun IncrementDecrement() { 98 | var counter by remember { mutableIntStateOf(0) } 99 | 100 | Button(onClick = { 101 | counter += 8 102 | }) { 103 | Text("increment") 104 | } 105 | Button(onClick = { 106 | counter -= 8 107 | }) { 108 | Text("decrement") 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/progress/ProgressBarViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.progress 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.asStateFlow 9 | import kotlinx.coroutines.launch 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class ProgressBarViewModel @Inject constructor() : 14 | ViewModel() { 15 | 16 | private val _progress = MutableStateFlow(0) 17 | val progress = _progress.asStateFlow() 18 | 19 | fun onStartPressed() { 20 | viewModelScope.launch { 21 | var counter = 0 22 | while (counter < 100) { 23 | val randomNo = (0..10).random() 24 | if (counter + randomNo > 100) continue 25 | counter += randomNo 26 | _progress.value = counter 27 | delay(500) 28 | } 29 | } 30 | } 31 | 32 | fun startLinearUpdate() { 33 | viewModelScope.launch { 34 | var counter = 0 35 | while (counter < 100) { 36 | counter += 10 37 | _progress.value = counter 38 | delay(500) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/search/SearchScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.search 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Text 7 | import androidx.compose.material3.TextField 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.collectAsState 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import androidx.hilt.navigation.compose.hiltViewModel 18 | 19 | 20 | @Composable 21 | fun SearchScreen( 22 | searchScreenViewModel: SearchScreenViewModel = hiltViewModel() 23 | ) { 24 | val searchReply by searchScreenViewModel.data.collectAsState(initial = "") 25 | 26 | SearchScreenContent( 27 | searchReply = searchReply, 28 | onSearchQueryChanged = searchScreenViewModel::onSearchQueryChanged 29 | ) 30 | 31 | } 32 | 33 | @Composable 34 | fun SearchScreenContent( 35 | searchReply: String, 36 | onSearchQueryChanged: (String) -> Unit, 37 | ) { 38 | var input by remember { mutableStateOf("") } 39 | 40 | Column( 41 | modifier = Modifier.fillMaxWidth(), 42 | horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally 43 | ) { 44 | TextField( 45 | value = input, 46 | onValueChange = { 47 | onSearchQueryChanged(it) 48 | input = it 49 | } 50 | ) 51 | Text( 52 | modifier = Modifier 53 | .padding(16.dp), 54 | text = searchReply 55 | ) 56 | } 57 | } 58 | 59 | 60 | @Preview(showBackground = true) 61 | @Composable 62 | fun SearchScreenPreview() { 63 | SearchScreenContent( 64 | searchReply = "Search reply", 65 | onSearchQueryChanged = {} 66 | ) 67 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/search/SearchScreenViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.search 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.FlowPreview 7 | import kotlinx.coroutines.delay 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.debounce 11 | import kotlinx.coroutines.flow.drop 12 | import kotlinx.coroutines.flow.flatMapLatest 13 | import kotlinx.coroutines.flow.flow 14 | import kotlinx.coroutines.flow.merge 15 | import nick.mirosh.androidsamples.ui.coroutines.cooperative_coroutine.TAG 16 | 17 | class SearchScreenViewModel() : ViewModel() { 18 | var _searchQuery = MutableStateFlow("Initial query") 19 | 20 | @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) 21 | var data2: Flow = merge( 22 | backendFlow("Initial flow"), 23 | _searchQuery.drop(1) 24 | .debounce(2000) 25 | .flatMapLatest { query -> 26 | backendFlow(query) 27 | }) 28 | 29 | var data: Flow = _searchQuery 30 | .debounceWithInitialCall( 31 | defaultValue = "Initial query", 32 | requestFunction = { 33 | backendFlow(it) 34 | } 35 | ) 36 | 37 | fun onSearchQueryChanged(query: String) { 38 | _searchQuery.value = query 39 | } 40 | 41 | private fun backendFlow(params: String) = flow { 42 | emit(params) 43 | } 44 | 45 | @OptIn(ExperimentalCoroutinesApi::class) 46 | var data3: Flow = _searchQuery 47 | .throttleFirst(2000) 48 | .flatMapLatest { query -> 49 | backendFlow(query) 50 | } 51 | 52 | 53 | } 54 | 55 | //fun Flow.throttleFirst(windowDuration: Long): Flow { 56 | // var job: Job = Job().apply { complete() } 57 | // 58 | // return onCompletion { job.cancel() }.run { 59 | // flow { 60 | // coroutineScope { 61 | // collect { value -> 62 | // if (!job.isActive) { 63 | // emit(value) 64 | // job = launch { delay(windowDuration) } 65 | // } 66 | // } 67 | // } 68 | // } 69 | // } 70 | //} 71 | 72 | @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) 73 | fun Flow.debounceWithInitialCall( 74 | defaultValue: T, 75 | requestFunction: (T) -> Flow, 76 | debounceTime: Long = 2000L 77 | ): Flow = 78 | merge( 79 | requestFunction(defaultValue), 80 | drop(1) 81 | .debounce(debounceTime) 82 | .flatMapLatest { 83 | requestFunction(it) 84 | } 85 | ) 86 | 87 | 88 | fun Flow.throttleFirst(windowDuration: Long): Flow = flow { 89 | var windowStartTime = System.currentTimeMillis() 90 | var emitted = false 91 | 92 | var lastEmittedValue: T? = null 93 | collect { value -> 94 | val currentTime = System.currentTimeMillis() 95 | val delta = currentTime - windowStartTime 96 | if (delta >= windowDuration) { 97 | windowStartTime += delta / windowDuration * windowDuration 98 | emitted = false 99 | if (lastEmittedValue != null && lastEmittedValue != value) { 100 | Log.d(TAG, "throttleFirst: emitting value because we're idle") 101 | emit(value) 102 | lastEmittedValue = value 103 | emitted = true 104 | } 105 | } 106 | if (!emitted) { 107 | emit(value) 108 | lastEmittedValue = value 109 | emitted = true 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/side_effects/SideEffectsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidcomposeexample.ui.sideeffects.launchedeffect 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.delay 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | import kotlinx.coroutines.launch 9 | 10 | const val UPDATE_DELAY_IN_DECISECONDS = 30 11 | 12 | class SideEffectsViewModel : ViewModel() { 13 | 14 | private val _progressMessage = MutableStateFlow("") 15 | val progressMessage = _progressMessage.asStateFlow() 16 | 17 | private val _messageToDisplay = MutableStateFlow<(() -> String)?>(null) 18 | val messageToDisplay = _messageToDisplay.asStateFlow() 19 | 20 | private val _timerValue = MutableStateFlow("") 21 | val timerValue = _timerValue.asStateFlow() 22 | 23 | private var initialMessage = "" 24 | private var newMessage = "" 25 | 26 | fun scheduleMessage(message: String) { 27 | initialMessage = message 28 | } 29 | 30 | fun scheduleUpdate(message: String) { 31 | newMessage = message 32 | startTimerAndUpdates() 33 | } 34 | 35 | /* 36 | In this function we're scheduling our initial message and an update message. 37 | We're also starting a timer that will count down from 8 seconds to 0 every decisecond. 38 | After UPDATE_DELAY_IN_DECISECONDS we're updating the message to display the new message. 39 | */ 40 | private fun startTimerAndUpdates() { 41 | _progressMessage.value = 42 | "Message \"$initialMessage\" is scheduled" 43 | _messageToDisplay.value = { initialMessage } 44 | viewModelScope.launch { 45 | for (i in (MESSAGE_DELAY / 100).toInt() downTo 0) { 46 | _timerValue.value = " ${i / 10},${i % 10}s" 47 | if (i == UPDATE_DELAY_IN_DECISECONDS) { 48 | _progressMessage.value = 49 | "Message is now \"$newMessage\"" 50 | _messageToDisplay.value = { newMessage } 51 | } 52 | delay(100) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/jetpack_compose/side_effects/disposable_effect/DisposableEffectScreen.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.jetpack_compose.side_effects.disposable_effect 2 | 3 | import android.util.Log 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.material.Button 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.DisposableEffect 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableIntStateOf 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import kotlinx.coroutines.CoroutineScope 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.delay 18 | import kotlinx.coroutines.launch 19 | 20 | private const val TAG = "DisposableEffectScreen" 21 | @Composable 22 | fun DisposableEffectScreen() { 23 | var userId by remember { mutableIntStateOf(0) } 24 | Column { 25 | Button(onClick = { 26 | userId++ 27 | }) { 28 | Text("change user id") 29 | } 30 | if (userId > 0) { 31 | UserProfileWithDisposableEffectBug(userId) 32 | } 33 | } 34 | } 35 | 36 | @Composable 37 | fun UserProfileWithDisposableEffectBug(userId: Int) { 38 | var profile by remember { mutableStateOf(null) } 39 | var isError by remember { mutableStateOf(false) } 40 | 41 | DisposableEffect(Unit) { 42 | val job = CoroutineScope(Dispatchers.IO) 43 | .launch { 44 | profile = fetchUserProfile(userId) 45 | } 46 | 47 | onDispose { 48 | job.cancel() 49 | } 50 | } 51 | 52 | LaunchedEffect(profile) { 53 | if (profile != null) { 54 | //do something with profile 55 | } else { 56 | isError = true 57 | } 58 | } 59 | 60 | if (isError) { 61 | Text("Error Loading Profile") 62 | } else { 63 | Text("Success") 64 | } 65 | } 66 | 67 | 68 | @Composable 69 | fun DisposableEffectCancel(userId: Int) { 70 | DisposableEffect(userId) { 71 | Log.d(TAG, "DisposableEffect: running") 72 | val job = CoroutineScope(Dispatchers.IO) 73 | .launch { 74 | delay(5000) 75 | Log.d(TAG, "DisposableEffect coroutine finished $userId") 76 | } 77 | 78 | onDispose { 79 | Log.d(TAG, "onDispose running") 80 | job.cancel() 81 | } 82 | } 83 | } 84 | 85 | data class UserProfile(val name: String) 86 | 87 | suspend fun fetchUserProfile(userId: Int): UserProfile { 88 | delay(5000) 89 | return UserProfile("Nick") 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/main/MainScreenContent.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.main 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.rememberScrollState 10 | import androidx.compose.foundation.verticalScroll 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import nick.mirosh.androidsamples.ui.theme.MyApplicationTheme 18 | 19 | @Composable 20 | fun MainScreenContent( 21 | modifier: Modifier = Modifier, 22 | onCoroutinesClicked: (() -> Unit)? = null, 23 | onComposeClicked: (() -> Unit)? = null, 24 | onAndroidApisClicked: (() -> Unit)? = null, 25 | 26 | ) { 27 | val scrollState = rememberScrollState() 28 | 29 | Column(modifier = modifier.verticalScroll(scrollState)) { 30 | Text( 31 | text = "Coroutines", 32 | modifier = Modifier 33 | .clickable { 34 | onCoroutinesClicked?.invoke() 35 | } 36 | .padding(24.dp) 37 | ) 38 | 39 | Text( 40 | text = "Compose", 41 | modifier = Modifier 42 | .clickable { 43 | onComposeClicked?.invoke() 44 | } 45 | .padding(24.dp) 46 | ) 47 | 48 | Text( 49 | text = "Android Apis", 50 | modifier = Modifier 51 | .clickable { 52 | onAndroidApisClicked?.invoke() 53 | } 54 | .padding(24.dp) 55 | ) 56 | } 57 | } 58 | 59 | 60 | @Preview 61 | @Composable 62 | fun ColumnPreview() { 63 | MyApplicationTheme { 64 | Box( 65 | modifier = Modifier 66 | .fillMaxSize() 67 | .background( 68 | Color.White 69 | ) 70 | ) 71 | MainScreenContent() 72 | } 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.main 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | import kotlinx.coroutines.launch 9 | import nick.mirosh.androidsamples.R 10 | import nick.mirosh.androidsamples.models.Pokemon 11 | import nick.mirosh.androidsamples.repository.PokemonRepository 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class MainViewModel @Inject constructor(private val pokemonRepository: PokemonRepository) : 16 | ViewModel() { 17 | 18 | private val _pokemonList = MutableStateFlow(listOf()) 19 | val pokemonList = _pokemonList.asStateFlow() 20 | 21 | init { 22 | viewModelScope.launch { 23 | _pokemonList.value = pokemonRepository.getPokemon().map { 24 | it.copy(color = getRandomColor()) 25 | } 26 | } 27 | } 28 | 29 | fun onDeleteItem(pokemon: Pokemon) { 30 | _pokemonList.value = _pokemonList.value.filter { it.name != pokemon.name } 31 | } 32 | 33 | private fun getRandomColor() = 34 | listOf( 35 | R.color.color1, 36 | R.color.color_2, 37 | R.color.color_3, 38 | R.color.color_4, 39 | R.color.color_5 40 | ).random() 41 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/navigation/ComposeNavGraph.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.navigation 2 | 3 | 4 | interface ComposeDestinations { 5 | val route: String 6 | } 7 | object ComposeLobbyScreenDestination : ComposeDestinations { 8 | override val route = "coroutine_lobby_screen" 9 | } 10 | object SimpleList : ComposeDestinations { 11 | override val route = "simple_list" 12 | } 13 | 14 | object ProgressBar : ComposeDestinations { 15 | override val route = "progress_bar" 16 | } 17 | 18 | object BottomNavigation : ComposeDestinations { 19 | override val route = "bottom_navigation" 20 | } 21 | 22 | object Animation : ComposeDestinations { 23 | override val route = "animation" 24 | } 25 | 26 | object LaunchedEffect : ComposeDestinations { 27 | override val route = "launched_effect" 28 | } 29 | 30 | object DisposableEffect : ComposeDestinations { 31 | override val route = "disposable_effect" 32 | } 33 | object ProduceState: ComposeDestinations { 34 | override val route = "produce_state" 35 | } 36 | 37 | object Parallax : ComposeDestinations { 38 | override val route = "parallax" 39 | } 40 | 41 | object ModifiersDestination : ComposeDestinations { 42 | override val route = "modifiers" 43 | } 44 | 45 | object MeasuringComposableDestination : ComposeDestinations { 46 | override val route = "measuring" 47 | } 48 | 49 | object SearchDestination : ComposeDestinations { 50 | override val route = "search" 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/navigation/CoroutineExceptionsNavGraph.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.navigation 2 | 3 | interface CoroutineExceptionDestinations { 4 | val route: String 5 | } 6 | 7 | object CoroutineExceptionsLobbyDestination : CoroutineExceptionDestinations { 8 | override val route = "exceptions_lobby" 9 | } 10 | 11 | object DifferentExceptionsChallengeDestination : CoroutineExceptionDestinations { 12 | override val route = "different_exceptions" 13 | } 14 | 15 | object ExceptionPropagationDestination : CoroutineExceptionDestinations { 16 | override val route = "different_exceptions" 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/navigation/CoroutineNavGraph.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.navigation 2 | 3 | interface CoroutinesDestinations { 4 | val route: String 5 | } 6 | 7 | object CoroutineLobbyScreenDestination : CoroutinesDestinations { 8 | override val route = "coroutine_lobby_screen" 9 | } 10 | 11 | object ExceptionsLobbyDestination : CoroutinesDestinations { 12 | override val route = "exception_lobby" 13 | } 14 | 15 | object CoroutineScopeDestination : CoroutinesDestinations { 16 | override val route = "coroutine_scope" 17 | } 18 | 19 | object CooperativeCancellationDestination : CoroutinesDestinations { 20 | override val route = "cooperative_cancellation" 21 | } 22 | 23 | object AsyncComparisonDestination : CoroutinesDestinations { 24 | override val route = "async" 25 | } 26 | 27 | object RememberCoroutineScopeDestination : CoroutinesDestinations { 28 | override val route = "remember_coroutine_scope" 29 | } 30 | 31 | object DeadLockDestination : CoroutinesDestinations { 32 | override val route = "deadlock" 33 | } 34 | 35 | object MultipleProcesses : CoroutinesDestinations { 36 | override val route = "multiple_processes" 37 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/navigation/MainNavGraph.kt: -------------------------------------------------------------------------------- 1 | 2 | package nick.mirosh.androidsamples.ui.navigation 3 | 4 | interface MainDestination { 5 | val route: String 6 | } 7 | 8 | object MainScreen : MainDestination { 9 | override val route = "main_screen" 10 | } 11 | 12 | object Coroutines : MainDestination { 13 | override val route = "remember_coroutine_scope" 14 | } 15 | 16 | object Compose : MainDestination { 17 | override val route = "compose" 18 | } 19 | 20 | object AndroidApis : MainDestination { 21 | override val route = "android_apis" 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun MyApplicationTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/nick/mirosh/androidsamples/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package nick.mirosh.androidsamples.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/picture1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/drawable/picture1.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/raw/amine_msiouri.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/raw/amine_msiouri.jpg -------------------------------------------------------------------------------- /app/src/main/res/raw/cancel3.json: -------------------------------------------------------------------------------- 1 | {"v":"5.7.1","fr":50,"ip":0,"op":100,"w":180,"h":180,"nm":"Composição 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Camada de forma 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":4,"ix":10},"p":{"a":0,"k":[95.93,78.377,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[157.064,150.1,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-19,27],[12.5,-11]],"c":false},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Forma 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":42,"s":[0]},{"t":55,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Aparar caminhos 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":42,"op":100,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Camada de forma 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":454,"ix":10},"p":{"a":0,"k":[100.976,95.709,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[151.67,141.837,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-19,27],[12.5,-11]],"c":false},"ix":2},"nm":"Caminho 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Forma 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":42,"s":[100]},{"t":55,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Aparar caminhos 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":42,"op":100,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Camada de forma 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[90,95,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.894,0.894,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"t":55,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[151,151],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.772549019608,0.188235294118,0.188235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[99.967,99.967],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[151,151],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.772549019608,0.188235294118,0.188235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[88.033,88.033],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":100,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Camada de forma 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[90,95,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"t":49,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[151,151],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.996078431373,0.843137254902,0.843137254902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[119.225,119.225],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[151,151],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Caminho da elipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Traçado 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.996078431373,0.843137254902,0.843137254902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Preenchimento 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[107.291,107.291],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"Elipse 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":100,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /app/src/main/res/raw/connor_danylenko.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/raw/connor_danylenko.jpg -------------------------------------------------------------------------------- /app/src/main/res/raw/fast_cancel.json: -------------------------------------------------------------------------------- 1 | {"v":"5.3.4","fr":60,"ip":79,"op":130,"w":130,"h":130,"nm":"Cancel JSON","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"cancel line 1","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[0.956,-1.526,0],"ix":2},"a":{"a":0,"k":[12.09,12.76,0],"ix":1},"s":{"a":0,"k":[87.717,87.717,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[24.18,0],[0,25.52]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6.091,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":89,"op":14489,"st":89,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"cancel line 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.956,-1.526,0],"ix":2},"a":{"a":0,"k":[12.09,12.76,0],"ix":1},"s":{"a":0,"k":[87.717,87.717,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[24.18,0],[0,25.52]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6.091,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":89,"op":14489,"st":89,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"pending symbol ctrl","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":89,"s":[0,0,100],"e":[130,130,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":99,"s":[130,130,100],"e":[80,80,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":109,"s":[80,80,100],"e":[100,100,100]},{"t":119}],"ix":6}},"ao":0,"ip":89,"op":14489,"st":89,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"pending yellow","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[0.989]},"o":{"x":[0.333],"y":[0]},"n":["0_0p989_0p333_0"],"t":89,"s":[0],"e":[100]},{"t":109}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[65,65,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[96,96],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.992156862745,0.360784313725,0.360784313725,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Mask","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":89,"op":14489,"st":89,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"green circle 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1.008]},"o":{"x":[0.333],"y":[0]},"n":["0_1p008_0p333_0"],"t":79,"s":[100],"e":[0]},{"t":119}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[65,65,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[0.994,0.994,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0_0p994_0p333_0","0_0p994_0p333_0","0p667_1_0p333_0"],"t":79,"s":[0,0,100],"e":[130,130,100]},{"t":119}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[96,96],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.964705882353,0.360784313725,0.372549019608,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Mask","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":79,"op":120,"st":39,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /app/src/main/res/raw/felix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/raw/felix.jpg -------------------------------------------------------------------------------- /app/src/main/res/raw/julia_volk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/raw/julia_volk.jpg -------------------------------------------------------------------------------- /app/src/main/res/raw/lukas_dlutko.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/raw/lukas_dlutko.jpg -------------------------------------------------------------------------------- /app/src/main/res/raw/matthew_montrone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/raw/matthew_montrone.jpg -------------------------------------------------------------------------------- /app/src/main/res/raw/pixabay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/raw/pixabay.jpg -------------------------------------------------------------------------------- /app/src/main/res/raw/sam_willis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsmirosh/Interactive-Android-Concepts/48042271720b596956ebfdb59f1ae544d790d652/app/src/main/res/raw/sam_willis.jpg -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | #9b5de5 12 | #f15bb5 13 | #fee440 14 | #00bbf9 15 | #00f5d4 16 | 17 | #55AD81 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | My Android Samples 3 | 4 | 5 | Launched Effect 6 | Use rememberUpdatedState 7 | Enter message 8 | Schedule 9 | Schedule a message to be sent in 6 seconds 10 | Send a new message instead of the old one after 3 seconds 11 | 12 | 13 | 14 | lateinit var job: Job\n 15 | fun startUncooperativeCoroutine() { \n 16 | \u0020 \u0020 \u0020 \u0020 job = GlobalScope.launch { \n 17 | \u0020 \u0020 \u0020 \u0020 \u0020 \u0020 \u0020 \u0020 while(true) { \n 18 | 19 | 20 | 21 | \u0020 \u0020 \u0020 \u0020 \u0020 \u0020 \u0020 \u0020 } \n 22 | \u0020 \u0020 \u0020 \u0020 } 23 | 24 | 25 | 26 | 27 | Box( 28 | modifier = Modifier\n 29 | .size(150.dp)\n 30 | .border(24.dp, Purple)\n 31 | .background(Yellow) 32 | .border(24.dp, Blue) 33 | .align(Alignment.Center) 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |