<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Damar Satria Buana on Medium]]></title>
        <description><![CDATA[Stories by Damar Satria Buana on Medium]]></description>
        <link>https://medium.com/@buanasatriaa?source=rss-224ccc9228ea------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*II3J9aoh__uScrc9uItZoA.png</url>
            <title>Stories by Damar Satria Buana on Medium</title>
            <link>https://medium.com/@buanasatriaa?source=rss-224ccc9228ea------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 15 May 2026 16:16:56 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@buanasatriaa/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Choosing the Right Date and Time Format in Android Development]]></title>
            <link>https://blog.stackademic.com/choosing-the-right-date-and-time-format-in-android-development-0c1e6379e17b?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/0c1e6379e17b</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[date-format]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[datetime]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Sat, 21 Dec 2024 10:54:43 GMT</pubDate>
            <atom:updated>2025-01-06T04:46:58.741Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5TJvODIWykfyKJsSdngtKg.png" /><figcaption>Banner — Choosing the Right Date and Time Format in Android Development</figcaption></figure><p>When developing an Android app, particularly when working with databases, you may often face a common question:</p><blockquote><strong>“Which date and time format should I use?”</strong></blockquote><p>As a junior developer, I struggled with this decision. A quick Google search led me to a discussion on StackOverflow from eight years ago: <a href="https://stackoverflow.com/questions/39546541/android-how-to-properly-store-date-and-time-in-sqlite">“How to properly store date and time in SQLite?”</a>. The insights were interesting, but I still sought clarity.</p><p>So, I turned to ChatGPT and asked:</p><blockquote><strong>“As a senior Android developer, how do you save your date-time data format in Android?”</strong></blockquote><p>Here’s what I learned from the responses and best practices followed by seasoned developers:</p><h3>Commonly Used Date and Time Formats</h3><h3>1. Timestamps (Milliseconds or Seconds)</h3><p>A timestamp is a numeric representation of the date and time, often the number of milliseconds or seconds since the Unix epoch (1970-01-01T00:00:00Z). In Android, this is commonly stored as a Long value.</p><h4>Example in Kotlin:</h4><pre>val millis: Long = 1732986000000 // Represents a specific date and time<br>// Or get the current timestamp<br>val millis = System.currentTimeMillis()</pre><h4>Pros:</h4><ul><li><strong>Efficient for computations:</strong> Easy to sort, compare, and perform date calculations.</li><li><strong>Compact:</strong> Takes up less space compared to strings.</li><li><strong>Time zone neutral:</strong> Always represents the same moment in time, regardless of location.</li></ul><h4>Cons:</h4><ul><li><strong>Not human-readable:</strong> Debugging timestamps can be challenging without converting them to a readable format.</li></ul><h3>2. ISO 8601 Strings</h3><p>ISO 8601 is a standardized, human-readable string format for dates and times, such as yyyy-MM-dd&#39;T&#39;HH:mm:ss&#39;Z&#39;. This is widely used in APIs and cross-platform systems.</p><h4>Example in Kotlin:</h4><pre>val isoDateString = Instant.now().toString() // Outputs &quot;2024-12-21T14:30:00Z&quot;</pre><h4>Pros:</h4><ul><li><strong>Readable and intuitive:</strong> Easier for humans to understand at a glance.</li><li><strong>Interoperable:</strong> Commonly used in backend APIs and across different systems.</li></ul><h4>Cons:</h4><ul><li><strong>Less efficient for operations:</strong> Sorting and calculations are more complex compared to timestamps.</li><li><strong>Larger storage size:</strong> Strings take more space than a numeric representation.</li></ul><h3>Additional Best Practices: Handling Time Zones</h3><p>When working with date and time data, handling time zones properly is crucial. Here’s how senior developers address this:</p><h3>1. Always Use UTC for Storage</h3><p>Save your dates and times in Coordinated Universal Time (UTC) to avoid inconsistencies caused by time zone differences.</p><h4>Example in Kotlin:</h4><pre>val utcTime = ZonedDateTime.now(ZoneOffset.UTC).toInstant()</pre><h3>2. Convert to Local Time for Display</h3><p>When presenting date and time to the user, convert UTC to the user’s local time zone. This ensures the displayed time is relevant to the user.</p><h4>Example in Kotlin:</h4><pre>val localTime = Instant.ofEpochMilli(timestamp)<br>    .atZone(ZoneId.systemDefault())<br>    .toLocalDateTime()</pre><h3>How to Choose the Right Format</h3><p>Here’s a simple guide to help you decide:</p><p><strong>Use Timestamps</strong> if:</p><ul><li>You’re working with databases (e.g., Room or SQLite) and need efficient sorting or filtering.</li><li>Space efficiency is a concern.</li></ul><p><strong>Use ISO 8601 Strings</strong> if:</p><ul><li>The format needs to be human-readable (e.g., for APIs or logs).</li><li>The data will be shared across systems with varying implementations.</li></ul><p>For most Android apps, using <strong>timestamps</strong> for storage and converting them to <strong>ISO 8601 strings</strong> or localized formats for display strikes a good balance.</p><h3>Wrapping Up</h3><p>Choosing the right date and time format depends on your app’s specific needs. By understanding the trade-offs between timestamps and ISO 8601 strings, as well as the importance of handling time zones correctly, you can design a robust and scalable app.</p><p>I hope this article helps you make informed decisions about date and time storage in your Android projects. Let me know your thoughts in the comments!</p><h3>Thank you for being a part of the community</h3><p><em>Before you go:</em></p><ul><li>Be sure to <strong>clap</strong> and <strong>follow</strong> the writer ️👏<strong>️️</strong></li><li>Follow us: <a href="https://x.com/inPlainEngHQ"><strong>X</strong></a> | <a href="https://www.linkedin.com/company/inplainenglish/"><strong>LinkedIn</strong></a> | <a href="https://www.youtube.com/channel/UCtipWUghju290NWcn8jhyAw"><strong>YouTube</strong></a> | <a href="https://newsletter.plainenglish.io/"><strong>Newsletter</strong></a> | <a href="https://open.spotify.com/show/7qxylRWKhvZwMz2WuEoua0"><strong>Podcast</strong></a></li><li><a href="https://cofeed.app/"><strong>Check out CoFeed, the smart way to stay up-to-date with the latest in tech</strong></a> <strong>🧪</strong></li><li><a href="https://differ.blog/"><strong>Start your own free AI-powered blog on Differ</strong></a> 🚀</li><li><a href="https://discord.gg/in-plain-english-709094664682340443"><strong>Join our content creators community on Discord</strong></a> 🧑🏻‍💻</li><li>For more content, visit <a href="https://plainenglish.io/"><strong>plainenglish.io</strong></a> + <a href="https://stackademic.com/"><strong>stackademic.com</strong></a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0c1e6379e17b" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/choosing-the-right-date-and-time-format-in-android-development-0c1e6379e17b">Choosing the Right Date and Time Format in Android Development</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Create an Expandable Card in Jetpack Compose]]></title>
            <link>https://blog.stackademic.com/create-an-expandable-card-in-jetpack-compose-69482fd016ff?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/69482fd016ff</guid>
            <category><![CDATA[material-design]]></category>
            <category><![CDATA[jetpack-compose]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Thu, 19 Dec 2024 07:47:32 GMT</pubDate>
            <atom:updated>2024-12-19T07:47:32.883Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ohf3vQPCuSjKZf38j98oiA.png" /><figcaption>Banner — Create an Expandable Card in Jetpack Compose</figcaption></figure><p>In this tutorial, I’ll guide you through creating a visually appealing and functional <strong>Expandable Card</strong> using Jetpack Compose. This design is perfect for scenarios where you want to group content and allow users to expand or collapse it for better visibility and interaction. Let’s dive in!</p><h3>What You’ll Build</h3><p>We’ll create a reusable DailyCashFlowCardItem component that displays a <strong>date</strong>, <strong>total income</strong>, and <strong>total expenses</strong>, along with an expandable list of categorized cash flows. The card allows users to toggle the visibility of its content and supports actions like editing or deleting individual items.</p><p>Here’s how the final design will look:</p><ul><li>Collapsed: Only the date and summary are visible.</li><li>Expanded: Displays detailed cash flow entries.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/534/1*Ng7XZb15hge7vLJ1a9ewIA.gif" /><figcaption>Result</figcaption></figure><h3>Key Features</h3><ol><li>Expand and collapse animations.</li><li>Reusable and easy to customize.</li></ol><h3>Step 1: Setting Up the Card Component</h3><p>The main component, DailyCashFlowCardItem, uses a Card with a Column to structure the content. We toggle the visibility of its detailed content using an <strong>AnimatedVisibility</strong> wrapper.</p><pre>@Composable<br>fun DailyCashFlowCardItem(<br>    modifier: Modifier = Modifier,<br>    date: String,<br>    totalExpenses: Double = 0.0,<br>    totalIncome: Double = 0.0,<br>    cashFlowList: List&lt;CashFlowAndCategory&gt;,<br>    onDeleteCashFlow: (CashFlow) -&gt; Unit,<br>    navigateToEditCashFlowScreen: (Int) -&gt; Unit<br>) {<br>    // State to hold the visibility of the content inside the card<br>    // to show wether its expanded or collapsed<br>    var itemRowVisible by remember { mutableStateOf(true) }<br>    <br>    Card(<br>        modifier = modifier.fillMaxWidth(),<br>        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),<br>        elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp)<br>    ) {<br>        Column {<br>            DailyCashFlowHeader(<br>                totalExpenses = totalExpenses,<br>                totalIncome = totalIncome,<br>                date = date,<br>                onClick = { itemRowVisible = !itemRowVisible }<br>            )<br>            AnimatedVisibility(visible = itemRowVisible) {<br>                HorizontalDivider()<br>            }<br>           // Toggles visibility based on the itemRowVisible state.<br>          // When itemRowVisible is true, the content is displayed; when false, it&#39;s hidden.<br>            AnimatedVisibility(<br>                visible = itemRowVisible,<br>                enter = expandVertically(animationSpec = tween(300)) + fadeIn(<br>                    animationSpec = tween(300)<br>                ), // Provides a smooth vertical expansion coupled with a gradual fade-in effect for enhanced visual appeal.<br>                exit = shrinkVertically(animationSpec = tween(300)) + fadeOut(<br>                    animationSpec = tween(300) // Combines a smooth vertical shrink with a fade-out effect, creating a polished transition during the collapse of the expanded view.<br>                )<br>            ) {<br>                Column {<br>                    cashFlowList.forEachIndexed { index, cashFlowAndCategory -&gt;<br>                        SwipeableItemWithActions(<br>                            isRevealed = cashFlowAndCategory.isOptionsRevealed,<br>                            actions = {<br>                                ActionIcon(<br>                                    icon = Icons.Default.Delete,<br>                                    tint = MaterialTheme.colorScheme.error,<br>                                    onClick = { onDeleteCashFlow(cashFlowAndCategory.cashFlow) }<br>                                )<br>                            },<br>                            onExpanded = {<br>                                cashFlowAndCategory.isOptionsRevealed = true<br>                            },<br>                            onCollapsed = {<br>                                cashFlowAndCategory.isOptionsRevealed = false<br>                            }<br>                        ) {<br>                            DailyCashFlowItemRow(<br>                                cashFlow = cashFlowAndCategory,<br>                                emoji = cashFlowAndCategory.category.emoji<br>                            )<br>                        }<br>                        if (index != cashFlowList.lastIndex) {<br>                            HorizontalDivider()<br>                        }<br>                    }<br>                }<br>            }<br>        }<br>    }<br>}</pre><h3>Step 2: Creating the Header</h3><p>The header displays the <strong>date</strong>, <strong>total income</strong>, and <strong>total expenses</strong>. Tapping the header toggles the expanded/collapsed state.</p><pre>@Composable<br>fun DailyCashFlowHeader(<br>    modifier: Modifier = Modifier,<br>    totalExpenses: Double,<br>    totalIncome: Double,<br>    date: String,<br>    onClick: () -&gt; Unit = {},<br>) {<br>    Column(modifier.clickable { onClick() }) {<br>        Row(<br>            modifier = Modifier<br>                .fillMaxWidth()<br>                .padding(12.dp)<br>        ) {<br>            Text(<br>                text = date,<br>                style = MaterialTheme.typography.labelLarge,<br>                color = MaterialTheme.colorScheme.outline<br>            )<br>            Spacer(modifier = Modifier.weight(1f))<br>            Text(<br>                text = totalIncome.formatToRupiah(),<br>                style = MaterialTheme.typography.titleSmall,<br>                color = MaterialTheme.colorScheme.tertiaryContainer<br>            )<br>            Spacer(modifier = Modifier.width(6.dp))<br>            Text(<br>                text = totalExpenses.formatToRupiah(),<br>                style = MaterialTheme.typography.titleSmall,<br>                color = MaterialTheme.colorScheme.error<br>            )<br>        }<br>    }<br>}</pre><h3>Step 3: Adding List Items</h3><p>The list items represent individual cash flow entries, including an <strong>emoji</strong>, <strong>category name</strong>, <strong>amount</strong>, and an optional <strong>note</strong>.</p><pre>@Composable<br>fun DailyCashFlowItemRow(<br>    modifier: Modifier = Modifier,<br>    cashFlow: CashFlowAndCategoryDomain,<br>    emoji: String<br>) {<br>    Row(<br>        modifier = modifier<br>            .padding(12.dp)<br>            .fillMaxWidth(),<br>        verticalAlignment = Alignment.CenterVertically<br>    ) {<br>        Box(<br>            modifier = Modifier<br>                .size(32.dp)<br>                .clip(RoundedCornerShape(4.dp))<br>                .background(MaterialTheme.colorScheme.tertiaryContainer)<br>        ) {<br>            Text(text = emoji, modifier = Modifier.align(Alignment.Center))<br>        }<br>        Spacer(modifier = Modifier.width(8.dp))<br>        Column(modifier = Modifier.weight(1f)) {<br>            Text(text = cashFlow.category.name, style = MaterialTheme.typography.titleMedium)<br>            if (cashFlow.cashFlow.note.isNotEmpty()) Text(<br>                text = cashFlow.cashFlow.note,<br>                color = MaterialTheme.colorScheme.outlineVariant,<br>                maxLines = 1,<br>                overflow = TextOverflow.Ellipsis,<br>                fontSize = 12.sp<br>            )<br>        }<br>        Text(<br>            text = cashFlow.cashFlow.amount.formatToRupiah(),<br>            style = MaterialTheme.typography.titleSmall,<br>            color = if (cashFlow.cashFlow.categoryId != 1) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.tertiaryContainer<br>        )<br>    }<br>}</pre><h3>Step 4: Adding Swipeable Actions</h3><p>You can add your own swipeable actions like deleting or editing items. This is achieved using a reusable SwipeableItemWithActions custom component.</p><h3>Conclusion</h3><p>In this tutorial, you learned how to build a polished expandable card in Jetpack Compose, complete with animations. This reusable component can enhance any app’s user experience by neatly organizing information.</p><p>Feel free to explore, modify, and share your version of this card design. Happy coding!</p><h3>Stackademic 🎓</h3><p>Thank you for reading until the end. Before you go:</p><ul><li>Please consider <strong>clapping</strong> and <strong>following</strong> the writer! 👏</li><li>Follow us <a href="https://twitter.com/stackademichq"><strong>X</strong></a> | <a href="https://www.linkedin.com/company/stackademic"><strong>LinkedIn</strong></a> | <a href="https://www.youtube.com/c/stackademic"><strong>YouTube</strong></a> | <a href="https://discord.gg/in-plain-english-709094664682340443"><strong>Discord</strong></a> | <a href="https://newsletter.plainenglish.io/"><strong>Newsletter</strong></a> | <a href="https://open.spotify.com/show/7qxylRWKhvZwMz2WuEoua0"><strong>Podcast</strong></a></li><li><a href="https://differ.blog/"><strong>Create a free AI-powered blog on Differ.</strong></a></li><li>More content at <a href="https://stackademic.com/"><strong>Stackademic.com</strong></a></li><li>Have a story to share? <a href="https://formulatools.co/f/cJh1CStlo9jJmhIegBM9"><strong>Join our global community of writers today!</strong></a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=69482fd016ff" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/create-an-expandable-card-in-jetpack-compose-69482fd016ff">Create an Expandable Card in Jetpack Compose</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Add Lottie Animation in Jetpack Compose]]></title>
            <link>https://blog.stackademic.com/how-to-add-lottie-animation-in-jetpack-compose-acd103b26e12?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/acd103b26e12</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[lottiefiles]]></category>
            <category><![CDATA[jetpack-compose]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Tue, 12 Nov 2024 01:06:00 GMT</pubDate>
            <atom:updated>2024-11-12T01:06:00.840Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bI0LyWpGnUQDs-78SifZ5w.png" /><figcaption>Banner — How to Add Lottie Animation in Jetpack Compose</figcaption></figure><p>Hey everyone! Today, I would like to share a step-by-step tutorial on how to add Lottie animations to an Android app using Jetpack Compose. Let’s dive in!</p><h4>What is Lottie Animation?</h4><p>Lottie is a JSON-based animation file format originally developed by Airbnb. It’s cross-platform, lightweight, and scalable, making it a perfect choice for high-quality animations on mobile apps without increasing app size significantly.</p><h4>Goal</h4><p>Our end goal is to create an app where users can enter a voucher code. If the code matches the correct one, a confetti animation will play; otherwise, nothing happens. Here’s what it’ll look like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*0bZPjxttgFW2QngJXfsW_g.gif" /><figcaption>Result — Confetti Animation</figcaption></figure><h4>1. Set Up Your Project</h4><p>Start by adding the Lottie library to your build.gradle file at the app module level:</p><pre>dependencies {<br><br>    // other dependencies<br><br>    // Lottie<br>    implementation(libs.lottie.compose)<br>}</pre><p>If you’re using Gradle version catalog, don’t forget to define the library name and version in libs.version.toml:</p><pre>[versions]<br>lottieCompose = &quot;6.6.0&quot;<br><br>[libraries]<br>lottie-compose = { module = &quot;com.airbnb.android:lottie-compose&quot;, version.ref = &quot;lottieCompose&quot; }</pre><p>Next, find a confetti animation on <a href="https://lottiefiles.com/search?q=confetti&amp;category=animations">LottieFiles</a> and download it. After downloading, create a “raw” folder under res in your project directory, and save the file there with a simple name like confetti.json.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tK-b9cNnZjMFzBaNIqE7sQ.png" /><figcaption>Example — Lottie Animation</figcaption></figure><h4>2. Creating the Animation App Composable</h4><p>Now, open MainActivity.kt and create a new composable function called ConfettiAnimationApp:</p><pre>@Composable<br>fun ConfettiAnimationApp(modifier: Modifier = Modifier) {<br><br>    val voucher = &quot;VOUCHER2024&quot;<br>    var isPlaying by remember { mutableStateOf(false) }<br>    var isError by remember { mutableStateOf(false) }<br><br>    Box(modifier) {<br>        AnimatedVisibility(visible = isPlaying) {<br>            ConfettiAnimation(<br>                modifier = Modifier<br>                    .fillMaxWidth()<br>                    .align(Alignment.TopCenter),<br>                isPlaying = isPlaying<br>            )<br>        }<br>        VoucherForm(<br>            modifier = Modifier<br>                .align(Alignment.Center)<br>                .padding(horizontal = 16.dp),<br>            onApply = { text -&gt;<br>                isError = text != voucher<br>                if (text.isNotEmpty() &amp;&amp; text == voucher) {<br>                    CoroutineScope(Dispatchers.Main).launch {<br>                        isPlaying = true<br>                        delay(3000L)<br>                        isPlaying = false<br>                    }<br>                }<br>            },<br>            isError = isError<br>        )<br>    }<br>}</pre><p><strong>Explanation:</strong></p><ul><li>ConfettiAnimationApp sets up the UI with a voucher input form and a confetti animation that plays when the form input matches the correct code.</li><li>AnimatedVisibility helps control whether the ConfettiAnimation is visible or hidden based on the isPlaying variable.</li><li>Finally, we set up VoucherForm to handle user input and validation.</li></ul><h4>3. Displaying the Lottie Animation</h4><p>This function is the core of the animation. It’s where we load and play the Lottie file.</p><pre>@Composable<br>fun ConfettiAnimation(modifier: Modifier = Modifier, isPlaying: Boolean = false) {<br>    val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.confetti))<br><br>    LottieAnimation(<br>        composition = composition,<br>        modifier = modifier,<br>        isPlaying = isPlaying,<br>    )<br>}</pre><p><strong>Explanation:</strong></p><ul><li>The composition variable stores the result of rememberLottieComposition, which loads the animation from R.raw.confetti.</li><li>We then call LottieAnimation, passing in composition and isPlaying to control when the animation starts or stops.</li></ul><h4>4. Building the Voucher Input Form</h4><p>The voucher input form handles the text input and checks if the voucher is correct.</p><pre>@Composable<br>fun VoucherForm(modifier: Modifier = Modifier, onApply: (value: String) -&gt; Unit = {}, isError: Boolean = false) {<br><br>    var text by remember { mutableStateOf(TextFieldValue(&quot;&quot;)) }<br><br>    Row(<br>        modifier.fillMaxWidth(),<br>        horizontalArrangement = Arrangement.SpaceBetween,<br>        verticalAlignment = Alignment.CenterVertically<br>    ) {<br>        OutlinedTextField(<br>            value = text,<br>            onValueChange = { value -&gt; text = value },<br>            modifier = Modifier.weight(1f),<br>            placeholder = {<br>                Text(text = &quot;Input Voucher&quot;)<br>            },<br>            isError = isError,<br>            )<br>        Spacer(modifier = Modifier.width(8.dp))<br>        Button(onClick = {<br>            onApply(text.text)<br>        }) {<br>            Text(text = &quot;Apply&quot;)<br>        }<br>    }<br>}</pre><p><strong>Explanation:</strong></p><ul><li>TextFieldValue manages form input.</li><li>isError is set to true when the voucher code doesn’t match.</li><li>A Button with onApply triggers the animation if the voucher is correct.</li></ul><h4>5. Adding Coroutine and Animation Control Logic</h4><p>To keep the animation running for only 3 seconds, we use a coroutine to handle timing.</p><pre> onApply = { text -&gt;<br>                isError = text != voucher<br>                if (text.isNotEmpty() &amp;&amp; text == voucher) {<br>                    CoroutineScope(Dispatchers.Main).launch {<br>                        isPlaying = true<br>                        delay(3000L)<br>                        isPlaying = false<br>                    }<br>                }<br>            },</pre><p>Using a coroutine here lets us set isPlaying to true for 3 seconds, then automatically set it to false. This gives a smooth transition for showing and hiding the animation.</p><h4>Conclusion</h4><p>And there you have it! With just a few lines of code, you’ve integrated a fun, responsive animation into your Jetpack Compose app. This approach can make any app feel more interactive and engaging without a lot of extra overhead. Try experimenting with different animations from LottieFiles or triggering animations based on other user actions for even more interactivity. Happy coding!</p><p>Explore:<br><a href="https://lottiefiles.com/what-is-lottie">What is a Lottie animation? — LottieFiles</a></p><h3>Stackademic 🎓</h3><p>Thank you for reading until the end. Before you go:</p><ul><li>Please consider <strong>clapping</strong> and <strong>following</strong> the writer! 👏</li><li>Follow us <a href="https://twitter.com/stackademichq"><strong>X</strong></a> | <a href="https://www.linkedin.com/company/stackademic"><strong>LinkedIn</strong></a> | <a href="https://www.youtube.com/c/stackademic"><strong>YouTube</strong></a> | <a href="https://discord.gg/in-plain-english-709094664682340443"><strong>Discord</strong></a> | <a href="https://newsletter.plainenglish.io/"><strong>Newsletter</strong></a> | <a href="https://open.spotify.com/show/7qxylRWKhvZwMz2WuEoua0"><strong>Podcast</strong></a></li><li><a href="https://differ.blog/"><strong>Create a free AI-powered blog on Differ.</strong></a></li><li>More content at <a href="https://stackademic.com/"><strong>Stackademic.com</strong></a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=acd103b26e12" width="1" height="1" alt=""><hr><p><a href="https://blog.stackademic.com/how-to-add-lottie-animation-in-jetpack-compose-acd103b26e12">How to Add Lottie Animation in Jetpack Compose</a> was originally published in <a href="https://blog.stackademic.com">Stackademic</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[String vs StringBuilder: Memory and Performance Analysis in Kotlin]]></title>
            <link>https://medium.com/@buanasatriaa/string-vs-stringbuilder-memory-and-performance-analysis-in-kotlin-113b8477ff72?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/113b8477ff72</guid>
            <category><![CDATA[string]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[kotlin-beginners]]></category>
            <category><![CDATA[analysis]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Thu, 07 Nov 2024 07:30:05 GMT</pubDate>
            <atom:updated>2024-11-07T07:30:05.647Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ua6dh5JjxalTH4hjIbFgoA.png" /><figcaption>Banner — String vs StringBuilder: Memory and Performance Analysis in Kotlin</figcaption></figure><p>As developers, we need to focus on writing high-quality, efficient code — code that runs faster and makes good use of memory. Today, I want to share some insights into a small but impactful choice: why you should use StringBuilder instead of String for certain types of string manipulation.</p><h3>Immutable vs Mutable Objects</h3><p>Let’s start with the basics. In Kotlin (and many other languages), the String class is immutable. That means once a String object is created, its value can’t be changed. Now, while immutability is useful for various reasons (like thread safety), it’s not always efficient when you need to modify a string repeatedly. That’s where StringBuilder comes in—a powerful class in Kotlin and Java that’s designed for mutable string manipulation. Using StringBuilder, you can modify strings without creating a new object each time, which can be a big performance booster.</p><h3>Comparing String vs StringBuilder: Memory and Performance</h3><p>To illustrate the difference between String and StringBuilder, let’s look at a quick performance test on memory and speed. Now, of course, the results here are specific to our test case; if you’re only doing light string manipulation, you may not need to worry about the difference between String and StringBuilder. But for heavy-duty string operations, the impact can be significant.</p><p>First, let’s create a function that concatenates a string 100.000 times using a String.</p><pre>@Test<br>fun measureBuildStringWithString() {<br>    println(buildStringWithString(loopCount))<br>}<br><br>private fun buildStringWithString(count: Int): String {<br>    var result = &quot;&quot;<br>    for (i in 1..count) {<br>        result += &quot;$i, &quot;<br>    }<br>    return result<br>}</pre><p>I ran this function in Android Studio and measured the execution time and memory usage. <strong>Took 4 sec 545 ms to finish the loop.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AD6DTQk0WNeOAXPyGxyNaA.png" /><figcaption>For loop string concatenation using String</figcaption></figure><p>Next, let’s try the same operation using StringBuilder:</p><pre>@Test<br>fun measureBuildStringWithStringBuilder() {<br>    println(buildStringWithStringBuilder(loopCount))<br>}<br><br>private fun buildStringWithStringBuilder(count: Int): String {<br>    val result = StringBuilder()<br>    for (i in 1..count) {<br>        result.append(i).append(&quot;,&quot;)<br>    }<br>    return result.toString()<br>}</pre><p>The results with StringBuilder show a considerable difference. <strong>Finish in just about 67 ms.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8zRT0ltDcoJAcW1uWHdVYQ.png" /><figcaption>For loop string concatenation using StringBuilder</figcaption></figure><p>To make it clear, I also created functions that measure the time and memory used by both methods so you can see the exact performance gains.</p><pre>private fun measurePerformance(method: Runnable) {<br>    val runtime = Runtime.getRuntime()<br>    runtime.gc()<br><br>    val memoryBefore = runtime.totalMemory() - runtime.freeMemory()<br>    val startTime = System.nanoTime()<br><br>    method.run()<br><br>    val endTime = System.nanoTime()<br>    val memoryAfter = runtime.totalMemory() - runtime.freeMemory()<br><br>    val memoryUsed = memoryAfter - memoryBefore<br>    val timeTaken = endTime - startTime<br><br>    println(&quot;Execution time (ms): &quot; + timeTaken / 1000000)<br>    println(&quot;Memory used (bytes: $memoryUsed&quot;)<br>}</pre><p>And here is the result:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/340/1*rcQjkSu1A-U9WNl57LGZaA.png" /></figure><h3>When and Why to Use StringBuilder</h3><p>So, when should you use StringBuilder? Here are some practical scenarios where it makes a noticeable difference:</p><ul><li><strong>Constructing long strings in loops</strong>: If you’re building up a long string inside a loop, StringBuilder is more efficient.</li><li><strong>Dynamic concatenation</strong>: When you’re not sure how many concatenations will be needed, StringBuilder handles these repeated operations better.</li><li><strong>Working with large data</strong>: For file operations, logging, or handling large data, StringBuilder minimizes memory usage and improves performance.</li></ul><h3>Conclusion</h3><p>In summary, using StringBuilder over String for repeated string manipulation can save a lot on both memory and execution time. String is perfectly fine for single or light concatenations, but once you’re dealing with multiple or complex concatenations, especially in loops, StringBuilder becomes the smarter choice. It’s a small tweak but can have a big impact on your app’s efficiency!</p><p>Source:</p><p><a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/">String — Kotlin Programming Language</a></p><p><a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/-string-builder/">StringBuilder — Kotlin Programming Language</a></p><p><a href="https://stackoverflow.com/questions/1532461/stringbuilder-vs-string-concatenation-in-tostring-in-java">performance — StringBuilder vs String concatenation in toString() in Java — Stack Overflow</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=113b8477ff72" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How To Create Image Slider and Custom Indicator in Jetpack Compose]]></title>
            <link>https://medium.com/@buanasatriaa/how-to-create-image-slider-and-custom-indicator-in-jetpack-compose-34bb0808f0a0?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/34bb0808f0a0</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[compose]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[jetpack-compose]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Tue, 29 Oct 2024 09:17:36 GMT</pubDate>
            <atom:updated>2024-10-29T09:17:36.157Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lWMPnSaxq2FSaIKi98OTPA.png" /><figcaption>Banner — How To Create Image Slider and Custom Indicator in Jetpack Compose</figcaption></figure><p>In this article, I would like to share how to build image slider with custom indicator in Jetpack Compose. It’s very simple and we don’t need external libraries to implement it.</p><p>Here is the final result:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*mpBrj0VM-pYoCkxifU9B_Q.gif" /><figcaption>ImagePager result</figcaption></figure><ol><li>Create Jetpack Compose project.</li><li>Create composable function ImagePager .</li></ol><pre>@Composable<br>fun ImagePager(<br>    modifier: Modifier = Modifier,<br>    pagerState: PagerState,<br>    navigateBack: () -&gt; Unit = {},<br>) {<br><br>    val images = /* List of images */<br><br>    Column(modifier) {<br>        Box(modifier = Modifier) {<br>            HorizontalPager(state = pagerState) { page -&gt;<br>                Image(<br>                    painter = painterResource(id = images[page]),<br>                    contentDescription = &quot;Image&quot;<br>                )<br>            }<br>            Indicator(<br>                dotCount = pagerState.pageCount,<br>                currentPage = pagerState.currentPage,<br>                dotSpacing = 4.dp,<br>                modifier = Modifier<br>                    .align(Alignment.BottomCenter)<br>                    .padding(bottom = 8.dp)<br>                    .height(12.dp)<br>            )<br>            IconButton(<br>                onClick = navigateBack, modifier = Modifier<br>                    .align(Alignment.TopStart)<br>                    .padding(8.dp)<br>            ) {<br>                Icon(<br>                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,<br>                    contentDescription = stringResource(<br>                        id = R.string.back<br>                    ),<br>                    tint = DarkNavy<br>                )<br>            }<br>        }<br><br>    }<br>}</pre><p>3. Then, we also need to create the Indicator composable function. We can modify the number of the dots, space between the dots, and also the size of the dots.</p><pre>@Composable<br>fun Indicator(<br>    dotCount: Int,<br>    currentPage: Int,<br>    modifier: Modifier = Modifier,<br>    dotSpacing: Dp = 8.dp,<br>    dotSize: Dp = 10.dp,<br>) {<br>    Row(<br>        horizontalArrangement = Arrangement.spacedBy(dotSpacing),<br>        verticalAlignment = Alignment.CenterVertically,<br>        modifier = modifier<br>    ) {<br>        for (i in 0 until dotCount) {<br>            val isActive = currentPage == i<br>            val color by animateColorAsState(targetValue =  if (isActive) Purple else DotIndicatorDefault,<br>                label = &quot;&quot;<br>            )<br>            val width by animateDpAsState(targetValue = if (isActive) dotSize * 3 else dotSize,<br>                label = &quot;&quot;<br>            )<br>            Box(<br>                modifier = Modifier<br>                    .width(width)<br>                    .height(dotSize)<br>                    .clip(CircleShape)<br>                    .background(color)<br>            )<br>        }<br>    }<br>}</pre><p>You can start to interact with the ImagePager result by call it inside the Preview annotation function.</p><pre>@Preview<br>@Composable<br>fun ImagePagerPreview() {<br>    ImagePager(shoeId = 1, pagerState = rememberPagerState {<br>        3<br>    })<br>}</pre><p>We use the rememberPagerState and pass 3 as its total page numbers that will be shown.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/436/1*lOfFQWauqJEdFSBUMObCQA.gif" /><figcaption>ImagePagerPreview</figcaption></figure><p>Now, you can create your own image slider with custom indicator.</p><p>Follow my social media for more Android tutorial: <a href="https://linktr.ee/notsatria">linktr.ee/notsatria</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=34bb0808f0a0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Human-Readable Date Format Using SimpleDateFormat in Android]]></title>
            <link>https://medium.com/@buanasatriaa/human-readable-date-format-using-simpledateformat-in-android-76d684093182?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/76d684093182</guid>
            <category><![CDATA[date]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[jetpack-compose]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[jetpack]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Fri, 11 Oct 2024 06:33:41 GMT</pubDate>
            <atom:updated>2024-10-11T06:33:41.876Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UqQaj3SgYjS62EcFSHQDXA.png" /><figcaption>Banner — Human-Readable Date Format Using SimpleDateFormat in Android</figcaption></figure><p>Hello Folks! While working with Android, especially if we want to improve our User Experience, we usually need to convert or parse the “machine” date format into “human” date format. For example, if you create an instance of a date in kotlin, you may see result just like below.</p><pre>fun main() {<br>   val date = Date()<br>   <br>   println(date)<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/330/1*9e1rDLCGssgSuogbIFSeFQ.png" /><figcaption>Date instance result</figcaption></figure><p>As a developer, we may not be confused about how to read the default date format, but for other people, it can be confusing. We need to change our mindset and put a little effort so all of user can read the date.</p><p>In this article, I hope you will learn these concepts:</p><ul><li>Get to know about SimpleDateFormat in Android.</li><li>How to implement SimpleDateFormat into our app.</li><li>Target a user specific region using Locale.</li></ul><blockquote>Let’s get started</blockquote><h3>What is SimpleDateFormat?</h3><p>Before we dive into the implementation, we need to understand the concept of SimpleDateFormat (SDF). Basically, SDF is a class that can be used to formatting and parsing dates in Android, and it is locale sensitive. There is a bunch of formats that can be used according to our need (<a href="https://developer.android.com/reference/android/icu/text/SimpleDateFormat">see here</a>). Below here are the examples of SDF that I personally often to use.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/998/1*QSEB0j29ROlohG4qPiJgRA.png" /><figcaption>Year formats</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/997/1*Ttgmjrrg4EtXvpIXjpv9JQ.png" /><figcaption>Month formats</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/996/1*v-Glo-iorVHhCHVGXv7u4A.png" /><figcaption>Date-Day and Day of week formats</figcaption></figure><h3>How to Implement SDF to Android App</h3><p>Like I said on above sentences, SDF support locale implementation.</p><blockquote>What does it mean?</blockquote><p>It means, you can implement date format in a specific region using <a href="https://developer.android.com/reference/java/util/Locale">Locale class</a>. Let’s take a look to the implementation below.</p><pre>@Composable<br>fun DateApp(modifier: Modifier = Modifier) {<br>    val currentDate = Date()<br>    val formattedDate = DateUtils.formatDate(currentDate, &quot;dd MMMM yyyy&quot;)<br>    Column(<br>        modifier.fillMaxSize(),<br>        horizontalAlignment = Alignment.CenterHorizontally,<br>        verticalArrangement = Arrangement.Center<br>    ) {<br>        Text(<br>            text = &quot;Default date: $currentDate&quot;,<br>            textAlign = TextAlign.Center,<br>            modifier = Modifier<br>        )<br>        Text(<br>            text = &quot;Formatted date: $formattedDate&quot;,<br>            textAlign = TextAlign.Center,<br>            modifier = Modifier<br>        )<br>    }<br>}</pre><p>If you see the code, there is two Text objects inside the column. It represents the default date and the formatted date. The result is shown below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/334/1*Yzp0HIJr6G3h1o7gYJh19w.png" /><figcaption>Default and formatted date</figcaption></figure><p>See right? The formatted date is more readable to user because we only show information that we want. It’s because we convert the currentDate inside the DateUtils.formatDate() function. Let’s take a look to the function.</p><pre>object DateUtils {<br><br>    fun formatDate(date: Date, format: String): String {<br>        val dateFormat = SimpleDateFormat(format, Locale.getDefault())<br>        return dateFormat.format(date)<br>    }<br>}</pre><p>The formatDate function has two parameters: date to input the date that we want to convert, and format is the SDF format that we want to show to the user. Like I said, SDF is locale-sensitive, so we also need to pass the Locale instance to the SimpleDateFormat class. And then, the function will return a String of formatted date using the SDF format function.</p><p>You can also convert a String formatted date using SDF parse function. But you’ll required to pass the inputFormatand outputFormat like so:</p><pre>fun String.formatStrDateTo(inputFormat: String, outputFormat: String): String {<br>        val sdf = SimpleDateFormat(inputFormat, locale)<br>        val date = sdf.parse(this)<br>        val newSdf = SimpleDateFormat(outputFormat, locale)<br>        return newSdf.format(date!!)<br>    }</pre><h3>Show Region-Specific Date Format Using Locale</h3><p>Once we successfully convert the “machine” date format to “human” readable format, one problem may appear.</p><blockquote>How can I set the date to my specific language? I want the date to be Indonesian!</blockquote><p>Okay, the solution is simple. All you need is to add your specific region using Locale. There is a bunch of languages provided by Android that you can use (<a href="https://developer.android.com/reference/java/util/Locale">see here</a>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/504/1*Qs7VjDrcm_Sfh57Axo044w.png" /><figcaption>Locale provided by Android</figcaption></figure><blockquote>I don’t see my language there. What should I do?</blockquote><p>You can set your own locale by following this <a href="https://www.localeplanet.com/icu/id-ID/index.html">website</a>. Here is the example if you want to use Bahasa Indonesia.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/837/1*xwAptxb23AH3vrXvXh2vHg.png" /><figcaption>Using custom Locale</figcaption></figure><p>You need to pass the value of language and country in string format. And now you can see the difference👇</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/316/1*1q2HShmd48ckjr6Z1Oo22w.png" /><figcaption>Result using custom Locale</figcaption></figure><p>Now, you can make your own custom human-readable date format in Android using SimpleDateFormat and Locale class. If you like this article, please follow me! Thank you</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=76d684093182" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How To Create Onboarding / Slider Page in Android Using ViewPager2 and Custom Indicator]]></title>
            <link>https://medium.com/@buanasatriaa/how-to-create-onboarding-slider-page-in-android-using-viewpager2-and-custom-indicator-90ea26bfd401?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/90ea26bfd401</guid>
            <category><![CDATA[custom-view-android]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[xml]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Wed, 09 Oct 2024 03:53:29 GMT</pubDate>
            <atom:updated>2024-10-09T03:53:29.618Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hQDKcv47QTD-FUw7tNOMzQ.png" /><figcaption>Banner — How To Create Onboarding Page in Android</figcaption></figure><p>Hello Folks! While creating Android projects, one of the most repeated features that I build is Onboarding page. For me, this feature is required because it’s the right time to slightly introduce our app. Today, I would like to give a tutorial how to create your own onboarding page using <strong>ViewPager2</strong>.</p><p>By following this tutorial, I hope you will understand these concepts:</p><ul><li>Understand what ViewPager2 is and why we use it.</li><li>Create custom onboarding page using ViewPager2.</li><li>Create custom indicator using Dot Indicator library.</li></ul><blockquote>Without further ado, let’s dive into the tutorial!</blockquote><h3>Prepare the design</h3><p>Here is the mockup design that we want to build on our app. But first, we need to understad what ViewPager2 is.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DBGGa0ns0Bgv6jk6qxAbWA.png" /><figcaption>Onboarding Mockup Design</figcaption></figure><h3>What is ViewPager2?</h3><p>ViewPager2 is Android library that is used to display views or fragment in swipeable format (<a href="https://developer.android.com/jetpack/androidx/releases/viewpager2">read here</a>). The reason why I love using ViewPager2 is also its compatibility to TabLayout (<a href="https://developer.android.com/guide/navigation/navigation-swipe-view-2">read here</a>) and its ease of use. Here is how you import ViewPager2 to your project:</p><p>Add this on your project inside build.gradle.kts (app level)</p><pre>implementation(libs.androidx.viewpager2)</pre><p>Also add this on your libs.version.toml file. And then, don’t forget to sync project.</p><pre>[versions]<br>viewpager2 = &quot;1.1.0&quot;<br><br>[libraries]<br>androidx-viewpager2 = { module = &quot;androidx.viewpager2:viewpager2&quot;, version.ref = &quot;viewpager2&quot; }</pre><h4>Create Onboarding Layout</h4><p>After we have done with the library, we need to create the onboarding layout by add a fragment named OnBoardingFragment. You can follow the xml code below.</p><pre>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;<br>&lt;androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;<br>    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;<br>    xmlns:tools=&quot;http://schemas.android.com/tools&quot;<br>    android:layout_width=&quot;match_parent&quot;<br>    android:layout_height=&quot;match_parent&quot;&gt;<br><br>    &lt;androidx.viewpager2.widget.ViewPager2<br>        android:id=&quot;@+id/vpOnboarding&quot;<br>        android:layout_width=&quot;match_parent&quot;<br>        android:layout_height=&quot;0dp&quot;<br>        app:layout_constraintBottom_toTopOf=&quot;@+id/guideline3&quot;<br>        app:layout_constraintEnd_toEndOf=&quot;parent&quot;<br>        app:layout_constraintStart_toStartOf=&quot;parent&quot;<br>        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&gt;<br><br><br>    &lt;ImageView<br>        android:id=&quot;@+id/ivOnboarding&quot;<br>        android:layout_width=&quot;0dp&quot;<br>        android:layout_height=&quot;0dp&quot;<br>        android:scaleType=&quot;centerCrop&quot;<br>        android:contentDescription=&quot;@null&quot;<br>        android:visibility=&quot;gone&quot;<br>        app:layout_constraintBottom_toTopOf=&quot;@id/cvOnboarding&quot;<br>        app:layout_constraintEnd_toEndOf=&quot;parent&quot;<br>        app:layout_constraintStart_toStartOf=&quot;parent&quot;<br>        app:layout_constraintTop_toTopOf=&quot;parent&quot;<br>        app:srcCompat=&quot;@drawable/il_onboarding_1&quot;<br>        tools:visibility=&quot;visible&quot; /&gt;<br><br>    &lt;androidx.cardview.widget.CardView<br>        android:id=&quot;@+id/cvOnboarding&quot;<br>        android:layout_width=&quot;match_parent&quot;<br>        android:layout_height=&quot;0dp&quot;<br>        android:backgroundTint=&quot;@color/navy_dark&quot;<br>        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;<br>        app:layout_constraintEnd_toEndOf=&quot;parent&quot;<br>        app:layout_constraintStart_toStartOf=&quot;parent&quot;<br>        app:layout_constraintTop_toTopOf=&quot;@+id/guideline3&quot;&gt;<br><br>        &lt;androidx.constraintlayout.widget.ConstraintLayout<br>            android:layout_width=&quot;match_parent&quot;<br>            android:layout_height=&quot;match_parent&quot;&gt;<br><br>            &lt;com.tbuonomo.viewpagerdotsindicator.DotsIndicator<br>                android:id=&quot;@+id/indicator&quot;<br>                android:layout_width=&quot;wrap_content&quot;<br>                android:layout_height=&quot;wrap_content&quot;<br>                app:dotsColor=&quot;@color/indigo&quot;<br>                app:dotsCornerRadius=&quot;8dp&quot;<br>                app:dotsSize=&quot;12dp&quot;<br>                app:dotsSpacing=&quot;4dp&quot;<br>                app:dotsWidthFactor=&quot;2.5&quot;<br>                app:layout_constraintBottom_toTopOf=&quot;@id/guideline7&quot;<br>                app:layout_constraintEnd_toEndOf=&quot;parent&quot;<br>                app:layout_constraintStart_toStartOf=&quot;parent&quot;<br>                app:layout_constraintTop_toBottomOf=&quot;@id/guideline8&quot;<br>                app:progressMode=&quot;false&quot;<br>                app:selectedDotColor=&quot;@color/text_white&quot; /&gt;<br><br>            &lt;TextView<br>                android:id=&quot;@+id/tvTitle&quot;<br>                style=&quot;@style/SourceSans.SemiBold&quot;<br>                android:layout_width=&quot;wrap_content&quot;<br>                android:layout_height=&quot;wrap_content&quot;<br>                android:layout_marginTop=&quot;24dp&quot;<br>                android:layout_marginBottom=&quot;8dp&quot;<br>                android:text=&quot;@string/title_onboarding_1&quot;<br>                android:textSize=&quot;24sp&quot;<br>                app:layout_constraintBottom_toTopOf=&quot;@+id/tvSubtitle&quot;<br>                app:layout_constraintEnd_toEndOf=&quot;parent&quot;<br>                app:layout_constraintStart_toStartOf=&quot;parent&quot;<br>                app:layout_constraintTop_toBottomOf=&quot;@id/indicator&quot;<br>                app:layout_constraintVertical_chainStyle=&quot;packed&quot; /&gt;<br><br>            &lt;TextView<br>                android:id=&quot;@+id/tvSubtitle&quot;<br>                style=&quot;@style/SourceSans&quot;<br>                android:layout_width=&quot;0dp&quot;<br>                android:layout_height=&quot;wrap_content&quot;<br>                android:layout_marginStart=&quot;24dp&quot;<br>                android:layout_marginEnd=&quot;24dp&quot;<br>                android:layout_marginBottom=&quot;16dp&quot;<br>                android:text=&quot;@string/subtitle_onboarding_1&quot;<br>                android:textAlignment=&quot;center&quot;<br>                android:textSize=&quot;16sp&quot;<br>                app:layout_constraintBottom_toBottomOf=&quot;@+id/materialDivider12&quot;<br>                app:layout_constraintEnd_toEndOf=&quot;parent&quot;<br>                app:layout_constraintStart_toStartOf=&quot;parent&quot;<br>                app:layout_constraintTop_toBottomOf=&quot;@id/tvTitle&quot; /&gt;<br><br>            &lt;com.google.android.material.divider.MaterialDivider<br>                android:id=&quot;@+id/materialDivider12&quot;<br>                android:layout_width=&quot;match_parent&quot;<br>                android:layout_height=&quot;1dp&quot;<br>                android:layout_marginHorizontal=&quot;16dp&quot;<br>                android:layout_marginTop=&quot;48dp&quot;<br>                app:dividerColor=&quot;@color/divider_color&quot;<br>                app:layout_constraintEnd_toEndOf=&quot;parent&quot;<br>                app:layout_constraintStart_toStartOf=&quot;parent&quot;<br>                app:layout_constraintTop_toBottomOf=&quot;@id/guideline2&quot; /&gt;<br><br>            &lt;com.google.android.material.button.MaterialButton<br>                android:id=&quot;@+id/btnLoginFull&quot;<br>                style=&quot;@style/Widget.App.Button&quot;<br>                android:layout_width=&quot;0dp&quot;<br>                android:layout_height=&quot;wrap_content&quot;<br>                android:layout_marginHorizontal=&quot;16dp&quot;<br>                android:layout_marginTop=&quot;25dp&quot;<br>                android:layout_marginBottom=&quot;24dp&quot;<br>                android:layout_weight=&quot;1&quot;<br>                android:orientation=&quot;horizontal&quot;<br>                android:text=&quot;@string/enter&quot;<br>                android:visibility=&quot;gone&quot;<br>                app:layout_constraintEnd_toEndOf=&quot;parent&quot;<br>                app:layout_constraintStart_toStartOf=&quot;parent&quot;<br>                app:layout_constraintTop_toBottomOf=&quot;@id/materialDivider12&quot; /&gt;<br><br>            &lt;LinearLayout<br>                android:id=&quot;@+id/llButton&quot;<br>                android:layout_width=&quot;match_parent&quot;<br>                android:layout_height=&quot;wrap_content&quot;<br>                android:layout_marginHorizontal=&quot;16dp&quot;<br>                android:layout_marginTop=&quot;25dp&quot;<br>                android:layout_marginBottom=&quot;24dp&quot;<br>                android:orientation=&quot;horizontal&quot;<br>                app:layout_constraintTop_toBottomOf=&quot;@id/materialDivider12&quot;&gt;<br><br>                &lt;com.google.android.material.button.MaterialButton<br>                    android:id=&quot;@+id/btnLogin&quot;<br>                    style=&quot;@style/Widget.App.Button&quot;<br>                    android:layout_width=&quot;0dp&quot;<br>                    android:layout_height=&quot;wrap_content&quot;<br>                    android:layout_weight=&quot;1&quot;<br>                    android:text=&quot;@string/enter&quot; /&gt;<br><br>                &lt;Button<br>                    android:id=&quot;@+id/btnNext&quot;<br>                    style=&quot;@style/Widget.App.Button.Outlined&quot;<br>                    android:layout_width=&quot;0dp&quot;<br>                    android:layout_height=&quot;wrap_content&quot;<br>                    android:layout_marginStart=&quot;8dp&quot;<br>                    android:layout_weight=&quot;1&quot;<br>                    android:text=&quot;@string/next&quot; /&gt;<br>            &lt;/LinearLayout&gt;<br><br>            &lt;androidx.constraintlayout.widget.Guideline<br>                android:id=&quot;@+id/guideline7&quot;<br>                android:layout_width=&quot;wrap_content&quot;<br>                android:layout_height=&quot;wrap_content&quot;<br>                android:orientation=&quot;horizontal&quot;<br>                app:layout_constraintGuide_begin=&quot;56dp&quot; /&gt;<br><br>            &lt;androidx.constraintlayout.widget.Guideline<br>                android:id=&quot;@+id/guideline8&quot;<br>                android:layout_width=&quot;wrap_content&quot;<br>                android:layout_height=&quot;wrap_content&quot;<br>                android:orientation=&quot;horizontal&quot;<br>                app:layout_constraintGuide_end=&quot;365dp&quot; /&gt;<br><br>            &lt;androidx.constraintlayout.widget.Guideline<br>                android:id=&quot;@+id/guideline2&quot;<br>                android:layout_width=&quot;wrap_content&quot;<br>                android:layout_height=&quot;wrap_content&quot;<br>                android:orientation=&quot;horizontal&quot;<br>                app:layout_constraintGuide_begin=&quot;170dp&quot; /&gt;<br><br>        &lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;<br>    &lt;/androidx.cardview.widget.CardView&gt;<br><br>    &lt;androidx.constraintlayout.widget.Guideline<br>        android:id=&quot;@+id/guideline3&quot;<br>        android:layout_width=&quot;wrap_content&quot;<br>        android:layout_height=&quot;wrap_content&quot;<br>        android:orientation=&quot;horizontal&quot;<br>        app:layout_constraintGuide_percent=&quot;0.59&quot; /&gt;<br><br><br>&lt;/androidx.constraintlayout.widget.ConstraintLayout&gt;</pre><h4>Create OnBoardingAdapter</h4><p>Just like RecyclerView, we need to use Adapter to communicate our ViewPager to our layout. Create the OnBoardingAdapter class that inherits from RecyclerView.Adapter You can provide your onboarding image list inside the class.</p><pre>class OnBoardingAdapter : RecyclerView.Adapter&lt;OnBoardingAdapter.ViewHolder&gt;() {<br><br>    private val imageList = listOf(<br>        R.drawable.il_onboarding_1,<br>        R.drawable.il_onboarding_2,<br>        R.drawable.il_onboarding_3<br>    )<br><br>    override fun onCreateViewHolder(<br>        parent: ViewGroup,<br>        viewType: Int<br>    ): ViewHolder {<br>        return ViewHolder(<br>            ItemOnboardingBinding.inflate(<br>                LayoutInflater.from(parent.context),<br>                parent,<br>                false<br>            )<br>        )<br>    }<br><br>    override fun onBindViewHolder(holder: ViewHolder, position: Int) {<br>        holder.binding.apply {<br>            ivOnboarding.setImageResource(imageList[position])<br>        }<br>    }<br><br>    override fun getItemCount(): Int {<br>        return imageList.size<br>    }<br><br>    inner class ViewHolder(val binding: ItemOnboardingBinding) :<br>        RecyclerView.ViewHolder(binding.root)<br>}</pre><p>You may see an error in the fragment_onboarding.xml file, especially on this &lt;com.tbuonomo.viewpagerdotsindicator.DotsIndicator It happens because wi didn’t add DotsIndicator library yet.</p><blockquote>Why do we need that library?</blockquote><p>It’s because we need to create a super-fast indicator that can communicate with our ViewPager without creating it from scratch. But, if you do research more, you can also use <a href="https://developer.android.com/reference/com/google/android/material/tabs/TabLayoutMediator">TabLayoutMediator</a>. For now, we’ll just use the library.</p><h4>Integrating Dot Indicator with ViewPager2</h4><p>I remember when I’m trying to research how to create custom indicator in Android and I found the library on <a href="https://github.com/tommybuonomo/dotsindicator">github</a>. It also compatible with the Jetpack Compose. First, we need to add the dependency.</p><p>Add this inside the build.gradle.kts (app level) and libs.versions.toml</p><pre>implementation(libs.dotsindicator)</pre><pre>[versions]<br>dotsindicator = &quot;5.0&quot;<br><br>[libraries]<br>dotsindicator = { module = &quot;com.tbuonomo:dotsindicator&quot;, version.ref = &quot;dotsindicator&quot; }</pre><h4>Combine the ViewPager2 and Dot Indicator</h4><p>After all of those setups, it’s time to intregating our ViewPager with the Dot Indicator inside our OnBoardingFragment view.</p><pre>class OnBoardingFragment : Fragment() {<br>    <br>    private var _binding: FragmentOnboardingBinding? = null<br>    private val binding: FragmentOnboardingBinding get() = _binding!!<br><br>    override fun onCreateView(<br>        inflater: LayoutInflater,<br>        container: ViewGroup?,<br>        savedInstanceState: Bundle?<br>    ): View {<br>        _binding = FragmentOnboardingBinding.inflate(inflater, container, false)<br>        return binding.root<br>    }<br><br>    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {<br>        super.onViewCreated(view, savedInstanceState)<br>        binding.apply { <br>            setupViewPager()<br>        }<br>    }<br><br>    private fun FragmentOnboardingBinding.setupViewPager() {<br>        val adapter = OnBoardingAdapter()<br>        vpOnboarding.adapter = adapter<br><br>        indicator.attachTo(vpOnboarding)<br><br>        vpOnboarding.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {<br>            override fun onPageSelected(position: Int) {<br>                super.onPageSelected(position)<br>                btnLoginFull.visibleIf(position == 2)<br>                llButton.visibleIf(position &lt; 2)<br><br>                var title = &quot;&quot;<br>                var subtitle = &quot;&quot;<br><br>                btnLogin.setOnClickListener {<br>                    startActivity(Intent(requireContext(), MainActivity::class.java))<br>                    requireActivity().finish()<br>                }<br><br>                when (position) {<br>                    0 -&gt; {<br>                        title = getString(R.string.title_onboarding_1)<br>                        subtitle = getString(R.string.subtitle_onboarding_1)<br>                        btnNext.setOnClickListener {<br>                            vpOnboarding.setCurrentItem(1, true)<br>                        }<br>                    }<br><br>                    1 -&gt; {<br>                        title = getString(R.string.title_onboarding_2)<br>                        subtitle = getString(R.string.subtitle_onboarding_2)<br>                        btnNext.setOnClickListener {<br>                            vpOnboarding.setCurrentItem(2, true)<br>                        }<br>                    }<br><br>                    2 -&gt; {<br>                        title = getString(R.string.title_onboarding_3)<br>                        subtitle = getString(R.string.subtitle_onboarding_3)<br>                        btnLoginFull.setOnClickListener {<br>                            startActivity(Intent(requireContext(), MainActivity::class.java))<br>                            requireActivity().finish()<br>                        }<br><br>                    }<br><br>                }<br>                tvTitle.text = title<br>                tvSubtitle.text = subtitle<br>            }<br>        })<br><br>    }<br><br>    override fun onDestroyView() {<br>        super.onDestroyView()<br>        _binding = null<br>    }<br>}</pre><p>Focus on the setupViewPager extension function. Here are the explanations:</p><ul><li>Create OnBoardingAdapter instance and assign it to the viewpager.</li><li>Attach the Dot Indicator to ViewPager by using the attachTo(viewPager2: ViewPager2) function,</li><li>Call the ViewPager callback function using the registerOnPageChangeCallback and override the onPageSelected(position: Int) function.</li><li>You can do whatever you want to, inside the callback function. For example, on the code I provided, I’m trying to change the title, subtitle, button visibility, and button functionalities inside.</li></ul><p>By following the step-by-step and build the app, the result can be shown below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*wdJeXmQERapyZqVOLG5TOA.gif" /><figcaption>Onboarding Page Result</figcaption></figure><p>And that’s it! Now you can create your own OnBoarding page using ViewPager2 and Dot Indicator.</p><blockquote>Follow me for more tips!</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=90ea26bfd401" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Glimpse Introduction into ViewModel in Android]]></title>
            <link>https://medium.com/@buanasatriaa/a-glimpse-introduction-into-viewmodel-in-android-1c6e673d101e?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/1c6e673d101e</guid>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android-architecture]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[viewmodel]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Thu, 03 Oct 2024 02:50:55 GMT</pubDate>
            <atom:updated>2024-10-03T02:50:55.583Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z0Tjo4XBxRdVL2T8npcXkw.png" /><figcaption>Banner — A Glimpse Introduction into ViewModel in Android</figcaption></figure><p>Saat awal-awal belajar pemrograman Android, Saya merasa “kelelahan” karena banyak sekali konsep yang harus dipelajari. Salah satunya adalah konsep tentang ViewModel. Bahkan, sebelum mengenal pemrograman Android sekalipun, Saya sering terpapar kata tersebut. Tidak jarang, kata-kata <strong>Model View ViewModel (MVVM)</strong> juga sering masuk melalui telinga Saya.</p><blockquote>Lantas, kenapa sih seakan-akan ViewModel ini sering digaungkan dan terkesan sangat penting? Yuk kita cari tahu!</blockquote><p>Dengan membaca artikel ini, diharapkan Anda dapat memahami beberapa hal berikut:</p><ul><li><strong>Memahami apa itu ViewModel</strong></li><li><strong>Melihat contoh kasus yang kerap dialami pemula saat belajar Android pertama kali</strong></li><li><strong>Mencoba mencari solusi dari masalah tersebut menggunakan ViewModel</strong></li></ul><blockquote>Sudah siap? Yuk kita mulai!</blockquote><h4>Apa itu ViewModel?</h4><p>Dalam perkembangannya, pemrograman Android didesain untuk memisahkan kode sesuai dengan kegunaannya, atau sering disebut sebagai <em>Separation of Concern</em> (SoC). ViewModel adalah sebuah <em>class</em> yang digunakan untuk mengatur <em>business logic</em> dan mempertahankan <em>state</em> pada <em>User Interface</em> (UI) aplikasi kita dari perubahan konfigurasi (<a href="https://developer.android.com/topic/libraries/architecture/viewmodel">Baca di sini</a>). Contohnya adalah ketika terjadi perubahan rotasi atau perubahan bahasa di dalam aplikasi.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*o0XS_OMAmivXSEHHKNgVZQ.png" /><figcaption>ViewModel as a State holders in UI Layer</figcaption></figure><p>Jadi intinya, ViewModel ini “ditugaskan” untuk menjaga <em>state</em> aplikasi supaya tetap bertahan. ViewModel merupakan bagian dari <a href="https://developer.android.com/topic/architecture">modern Android architecture</a>. ViewModel juga sering kali menjadi “jembatan” antara UI dengan data (misalnya data dari API, <em>Local Database</em>, atau <em>Shared Preferences</em>). Dalam penerapannya, ViewModel kerap didampingi oleh observer seperti LiveData, atau coroutine seperti StateFlow untuk manajemen data.</p><h4>Masalah yang dihadapi saat mengembangkan aplikasi Android pertama kali</h4><p>Studi kasus yang kerap kali dihadapi oleh orang yang pertama kali belajar Android adalah mempertahankan <em>state </em>aplikasi ketika terjadi perubahan konfigurasi.</p><blockquote>Maksudnya gimana tuh?</blockquote><p>Kita ambil contoh pada aplikasi yang pernah kita buat sebelumnya pada artikel saya ini <a href="https://medium.com/@buanasatriaa/how-to-create-custom-currency-rupiah-textfield-in-android-5c02fff10a5a">How To Create Custom Currency (Rupiah) TextField in Android</a>. Seperti yang bisa kita lihat, aplikasi tampak berjalan normal, hingga Anda melakukan rotasi seperti di bawah ini.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/498/1*NP8aJ61GHCYck7YiEInLRg.gif" /><figcaption>Contoh configuration change: Rotasi layar</figcaption></figure><blockquote>Lho kok ilang bwang?</blockquote><p>Hal ini dapat terjadi karena ketika terjadi perubahan konfigurasi seperti rotasi layar, aplikasi akan membuat <em>instance </em>Activity yang baru. Sehingga aplikasi kita memanggil <em>method </em>onDestroy() kemudian onCreate() yang berakibat pada hilangnya <em>state </em>aplikasi kita. (<a href="https://developer.android.com/guide/topics/resources/runtime-changes">Baca di sini</a>)</p><h4>Solusi Menggunakan ViewModel</h4><p>Masalah yang terjadi di atas tentu menghasilkan <em>User Experience</em> (UX) yang buruk bagi pengguna. Sebagai developer yang baik, Anda harus segera mencari solusi untuk meng-<em>handle state</em> tersebut supaya pengguna tidak segera meng-<em>uninstall </em>aplikasi kita. Untuk kasus ini, kita bisa menggunakan ViewModel sebagai solusi. Implementasinya seperti di bawah ini.</p><p>Kita harus membuat <em>class </em>terpisah bernama MainViewModel, nama ini ditentukan dari nama masing-masing <em>view</em>. Android merekomendasikan untuk setiap satu <em>view </em>memiliki satu buah ViewModel.</p><pre>import androidx.lifecycle.ViewModel<br><br>class MainViewModel : ViewModel() {<br>}</pre><p>Kita bisa mulai menuliskan business logic dari aplikasi kita. Misalkan pada kasus di atas, kita ingin menyimpan nilai value yang dihasilkan oleh edtPrice. ViewModel biasanya dikombinasikan dengan <em>observer </em>seperti LiveData atau StateFlow.</p><pre>class MainViewModel : ViewModel() {<br>    private var _price = MutableLiveData&lt;String&gt;()<br>    val price: LiveData&lt;String&gt; get() = _price<br><br>    fun setPrice(price: String) {<br>        _price.value = price<br>    }<br>}</pre><p>Untuk memanggil ViewModel ke dalam view, Anda bisa menggunakan beberapa cara. Salah satunya adalah dengan menggunakan <a href="https://kotlinlang.org/docs/delegated-properties.html">delegated properties</a> seperti di bawah ini.</p><pre>class MainActivity : AppCompatActivity() {<br><br>    private val mainViewModel: MainViewModel by viewModels()<br><br>}</pre><p>Kemudian, untuk menerapkan <em>logic </em>yang telah kita buat, bisa dilakukan seperti di bawah ini.</p><pre>class MainActivity : AppCompatActivity() {<br><br>    private val mainViewModel: MainViewModel by viewModels()<br><br>    private lateinit var edtPrice: EditText<br>    private lateinit var tvValue: TextView<br><br>    override fun onCreate(savedInstanceState: Bundle?) {<br>        super.onCreate(savedInstanceState)<br>        enableEdgeToEdge()<br>        setContentView(R.layout.activity_main)<br><br>        tvValue = findViewById(R.id.tvValue)<br>        edtPrice = findViewById(R.id.edtPrice)<br><br>        // set on done ime action<br>        edtPrice.setOnEditorActionListener { _, actionId, _ -&gt;<br>            if (actionId == EditorInfo.IME_ACTION_DONE) {<br>                mainViewModel.setPrice(edtPrice.text.toString())<br>            }<br>            true<br>        }<br><br>        mainViewModel.price.observe(this) { price -&gt;<br>            tvValue.text = getString(R.string.value, price)<br>        }<br><br>    }<br>}</pre><p>Penyimpanan nilai priceterjadi saat pemanggilan <em>function </em>mainViewModel.setPrice(edtPrice.text.toString())</p><pre>edtPrice.setOnEditorActionListener { _, actionId, _ -&gt;<br>            if (actionId == EditorInfo.IME_ACTION_DONE) {<br>                mainViewModel.setPrice(edtPrice.text.toString())<br>            }<br>            true<br>        }</pre><p>Kemudian, nilai dari price diakses menggunakan <em>observer</em> bawaan dari LiveData yang menghasilkan nilai <em>return </em>price</p><pre>mainViewModel.price.observe(this) { price -&gt;<br>            tvValue.text = getString(R.string.value, price)<br>        }</pre><p>Setelah itu, kita coba <em>test </em>aplikasi kita. Apakah hasilnya berubah atau masih sama.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/502/1*LopC91qUR8YBtwzvMoo4mA.gif" /><figcaption>Hasil setelah implement ViewModel</figcaption></figure><p>Sekarang Anda bisa membuat UX tetap terjaga dengan baik menggunakan ViewModel.</p><h4>Kesimpulan</h4><ul><li>ViewModel merupakan bagian dari Modern Android Architecture yang bertugas untuk mengatur <em>business logic </em>dan mempertahankan <em>state </em>aplikasi.</li><li>Sebelum menerapkan ViewModel, <em>state </em>aplikasi dapat “terhapus” dengan adanya perubahan konfigurasi seperti rotasi layar.</li><li>ViewModel biasanya diterapkan dengan LiveData atau StateFlow sebagai observer.</li><li>Disarankan untuk menggunakan satu ViewModel untuk setiap view.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1c6e673d101e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Old Way of Implementing Manual Dependency Injection in Android]]></title>
            <link>https://medium.com/@buanasatriaa/the-old-way-of-implementing-manual-dependency-injection-in-android-ad291d9baf71?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/ad291d9baf71</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[dependency-injection]]></category>
            <category><![CDATA[oop-concepts]]></category>
            <category><![CDATA[solid]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Tue, 24 Sep 2024 07:00:48 GMT</pubDate>
            <atom:updated>2024-09-24T07:00:48.233Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M0byp14To5tjkuUZm35QdQ.png" /><figcaption>Banner — The Old Way of Implementing Manual Dependency Injection in Android</figcaption></figure><p>Saat mengembangkan aplikasi Android, Anda akan menemukan satu konsep bernama <strong><em>Dependency Injection (DI)</em>. </strong>Ini adalah sebuah konsep dasar yang harus Anda kuasai dalam mengembangkan aplikasi Android.</p><h4>Apa itu Dependency Injection?</h4><p>Secara singkat, konsep ini biasa saya definisikan sebagai “ketergantungan” antara satu <em>class </em>dengan <em>class</em> lain. Seperti contoh yang tertulis pada laman blog resmi Android berikut <a href="https://developer.android.com/training/dependency-injection">Dependency injection in Android | Android Developers</a>.</p><h4>Contoh Kasus</h4><p>Ketika kita ingin mendefinisikan class Car yang bergantung pada class Engine. Kode yang tidak menerapkan konsep DI, terlihat seperti di bawah ini.</p><pre>class Car {<br>  private val engine = Engine()<br>  <br>  fun start() {<br>    engine.start()<br>  }<br>}<br><br>fun main() {<br>  val car = Car()<br>  car.start()   <br>}</pre><p>Terlihat seperti tidak ada yang salah, bukan?. Padahal, jika kita perhatikan lebih dalam, kode di atas akan membuat class Car sangat bergantung pada class Engine. Ini akan mempersulit kita saat ingin memisahkan <em>Car </em>menjadi (misalkan) <em>Electric Car</em> dan <em>Gas Car</em>. Padahal, keduanya hanya berbeda pada <em>engine</em>-nya saja.</p><blockquote>Lalu, bagaimana solusinya?</blockquote><p>Solusinya adalah dengan membuat class Car tidak bergantung secara langsung dengan class Engine, atau dikenal dengan istilah <em>loosely coupled.</em></p><pre>class Car(private val engine: Engine) {<br>  fun start() {<br>    engine.start()<br>  }<br>}<br><br>fun main() {<br>  val engine = Engine()<br>  val car = Car(engine)<br>  car.start()<br>}</pre><p>Dengan begitu, Anda juga bisa saja membuat <em>Electric Car</em> atau <em>Gas Car</em> secara bebas. Seperti pada contoh di bawah ini.</p><pre>class ElectricEngine : Engine() {}<br><br>class GasEngine : Engine() {}<br><br>class Car(private val engine: Engine) {<br>  fun start() {<br>    engine.start()<br>  }<br>}<br><br>fun main() {<br>  val electricEngine = ElectricEngine()<br>  val gasEngine = GasEngine()<br><br>  val electricCar = Car(electricEngine)<br>  val gasCar = Car(gasEngine)<br><br>  electricCar.start()<br>  gasCar.start()<br>}</pre><h3>Contoh Implementasi Manual Dependeny Injection</h3><p>Implementasi Manual DI akan sering kita temui saat mulai mengimplementasikan beberapa fitur di aplikasi kita, seperti local database<em>,</em> n<em>etwork connection, preferences,</em> dan lain-lain. Contoh penerapan pada <em>local database </em>adalah sebagai berikut.</p><p>Database yang saya gunakan adalah Room (<a href="https://developer.android.com/jetpack/androidx/releases/room">baca di sini</a>). Sederhananya, urutannya adalah sebagai berikut:</p><ol><li>Membuat instance database.</li><li>Membuat<strong><em> Injection/Service Locator</em></strong> untuk mem-<em>provide instance </em>dari database ke dalam <em>repository</em>.</li><li>Memanggil <strong><em>Service Locator</em></strong> di dalam ViewModel melalui ViewModelFactory</li></ol><h4>Membuat instance database melalui <em>function </em>getInstance()</h4><pre>@Database(entities = [TransactionEntity::class], version = 1)<br>@TypeConverters(Converters::class)<br>abstract class TransactionDatabase : RoomDatabase() {<br>    abstract fun dao(): TransactionDao<br><br>    companion object {<br>        @Volatile<br>        private var INSTANCE: TransactionDatabase? = null<br><br>        @JvmStatic<br>        fun getInstance(context: Context): TransactionDatabase {<br>            if (INSTANCE == null) {<br>                synchronized(TransactionDatabase::class.java) {<br>                    INSTANCE = Room.databaseBuilder(<br>                        context.applicationContext,<br>                        TransactionDatabase::class.java,<br>                        &quot;transaction.db&quot;<br>                    )<br>                        .build()<br>                }<br>            }<br>            return INSTANCE as TransactionDatabase<br>        }<br>    }<br>}</pre><h4>Jangan lupa untuk membuat <em>Data Access Object (DAO) </em>beserta <em>Entity</em>-nya.</h4><pre>interface TransactionDao {<br>    // Some DAO implementations<br>}<br><br>@Entity(tableName = &quot;transaction&quot;)<br>@Parcelize<br>data class TransactionEntity(<br>    // Entities property<br>) : Parcelable</pre><h4>Membuat TransactionRepository</h4><p>Perlu diperhatikan bahwa di sini saya juga menggunakan AppExecutor yang digunakan untuk meng-eksekusi <em>database </em>pada<em> Thread </em>yang berbeda (<a href="https://developer.android.com/reference/java/util/concurrent/Executor">Baca di sini</a>).</p><pre>class TransactionRepository private constructor(<br>    private val dao: TransactionDao,<br>    private val appExecutors: AppExecutors<br>) {<br>    // Implement logics here<br><br>    companion object {<br>        fun getInstance(<br>            dao: TransactionDao,<br>            appExecutors: AppExecutors,<br>        ): TransactionRepository {<br>            return TransactionRepository(dao, appExecutors)<br>        }<br>    }<br>}</pre><h4>Buat Injection</h4><p><em>Function</em> provideTransactionRepository ini lah yang akan menjadi kunci utama dari manual DI. Inti dari <em>function </em>ini adalah membuat instance TransactionRepository yang akan dibutuhkan nanti.</p><pre>object Injection {<br>    fun provideTransactionRepository(context: Context): TransactionRepository {<br>        val db = TransactionDatabase.getInstance(context)<br>        val dao = db.dao()<br>        val appExecutors = AppExecutors()<br><br>        return TransactionRepository.getInstance(dao, appExecutors)<br>    }<br>}</pre><h4>Menggunakan Injection</h4><p>KarenaTransactionViewModel membutuhkan <em>instance </em>dari TransactionRepository di sini, kita perlu memanfaatkan penggunaan ViewModelFactory pada kode kita. Hal ini karena proses dependensi yang sedikit berbeda pada ViewModel sehingga diperlukan mekanisme tambahan (<a href="https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-factories">Baca di sini</a>).</p><pre>class TransactionViewModel(private val repository: TransactionRepository) : ViewModel() {<br>    // Some logics here<br>}</pre><pre>class ViewModelFactory private constructor(<br>    private val transactionRepository: TransactionRepository<br>) : ViewModelProvider.NewInstanceFactory() {<br>    override fun &lt;T : ViewModel&gt; create(modelClass: Class&lt;T&gt;): T {<br>        return when (modelClass) {<br>            TransactionViewModel::class.java -&gt; TransactionViewModel(transactionRepository) <br>            else -&gt; throw IllegalArgumentException(&quot;Unknown ViewModel class: ${modelClass.name}&quot;)<br>        }<br>    }<br><br>    companion object {<br>        fun getInstance(context: Context): ViewModelFactory =<br>            ViewModelFactory(Injection.provideTransactionRepository(context))<br>    }<br>}</pre><p>Jika Anda perhatikan, proses Manual DI terjadi saat pembuatan instance ViewModelFactory di atas (perhatikan pada function getInstance()). Pada <em>constructor</em> ViewModelFactory kita <em>inject </em>dengan <em>function </em>yang kita telah kita buat sebelumnya untuk mem-<em>provide </em>TransactionRepository.</p><h4>Panggil ViewModel Di Dalam View</h4><p>Setelahnya, kita dapat memanggil TransactionViewModel dengan bebas di dalam view kita, tanpa “memperhatikan” cara pembuatan <em>instance </em>dari View Model tersebut.</p><pre>class TransactionFragment : Fragment() {<br><br>    private val viewModel by viewModels&lt;TransactionViewModel&gt; {<br>        ViewModelFactory.getInstance(requireContext())<br>    }<br>}</pre><blockquote>Simple kan?</blockquote><h4>Penutup</h4><p>Perlu diperhatikan bahwa cara di atas bukan merupakan cara terbaik dalam menerapkan konsep <em>Dependency Injection (DI). </em>Jika Anda pelajari lebih dalam lagi, Anda akan menemukan cara-cara DI yang lebih <em>advance </em>seperti penggunaan Dagger, Dagger-Hilt, dan Koin sebagai <em>library </em>yang dapat membuat proses DI menjadi jauh lebih mudah. Namun, tulisan saya di atas bisa menjadi acuan Anda untuk mempelajari konsep dan memahami <em>Dependency Injection</em> beserta dengan implementasinya.</p><p>Terhubung dengan penulis👇</p><p><a href="https://linktr.ee/notsatria">Satria — Android Developer | Instagram, TikTok | Linktree</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ad291d9baf71" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Belajar Typing Ganteng Dengan Extension Function di Kotlin]]></title>
            <link>https://medium.com/@buanasatriaa/belajar-typing-ganteng-dengan-extension-function-di-kotlin-0707f2b64720?source=rss-224ccc9228ea------2</link>
            <guid isPermaLink="false">https://medium.com/p/0707f2b64720</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Damar Satria Buana]]></dc:creator>
            <pubDate>Thu, 19 Sep 2024 06:11:07 GMT</pubDate>
            <atom:updated>2024-09-19T06:11:07.814Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SOWOtna_sA8FN9G2La9Z3w.png" /><figcaption>Banner — Belajar Typing Ganteng Dengan Extension Function di Kotlin</figcaption></figure><p>Sebagian dari kita pasti tau arti dari <em>Typing Ganteng </em>— Ketikan yang disukai oleh kebanyakan orang zaman sekarang wkwk <a href="https://communication.uii.ac.id/benarkah-typing-ganteng-sebagai-identitas-dan-perjalanan-gen-z-di-media-sosial/">(Baca tentang typing ganteng)</a>. Sebagai pengantar, dulu saya tidak suka dengan Kotlin. Alasannya adalah karena bahasanya yang ribet, susah dimengerti dan banyak fitur yang terlalu canggih😿.</p><p>Namun, sekarang saya mencoba untuk mengubah <em>mindset </em>tersebut menjadi “Kotlin bahasa yang ribet dan canggih, kalo Saya bisa menguasainya, artinya saya keren dan ikut canggih”. Benar saja, setelah saya mencoba untuk memahami Kotlin, saya semakin suka (dan benci juga sih) dengan bahasa ini😽.</p><p>Salah satu fitur yang ada pada Kotlin adalah Extension Function.</p><blockquote>Hah, apaan tuh? Kagak pernah denger sebelumnya.</blockquote><p>Jadi, intinya Extension Function adalah fitur yang memungkinkan kita untuk membuat <em>function </em>dari <em>class </em>atau <em>interface </em>yang sudah ada, tanpa memodifikasi atau membuat <em>class </em>baru yang meng-<em>inherit class </em>tersebut (<a href="https://kotlinlang.org/docs/extensions.html">Extension Function</a>).</p><h4>Coba kita lihat contohnya</h4><p>Perhatikan pada tulisan saya tentang <a href="https://medium.com/@buanasatriaa/how-to-handle-custom-radio-button-layout-without-using-radio-group-28ecd468bf28">Custom Radio Button</a>. Jika Anda perhatikan, di sana terdapat <em>function </em>bernama <strong>visibleIf </strong>yang memungkinkan kita untuk mengatur visibilitas dari satu <em>view </em>berdasarkan kondisi tertentu<strong>. </strong>Padahal, <em>function </em>tersebut tidak terdapat dalam Kotlin itu sendiri.</p><p>Sebelum mengenal Extension Function, saya menulis kode untuk mengatur visibilitas <em>view</em> sebagai berikut.</p><pre>fun updateVisibility(view: View, isVisible: Boolean) {<br>    view.visibility = if (isVisible) View.VISIBLE else View.GONE<br>}</pre><p>Sebetulnya, tidak ada masalah dengan kode tersebut. Namun, karena kita di sini ingin lebih menjadi tampan, ganteng, dan sigma. Oleh karenanya, kita coba menerapkan <em>Extension Function</em> seperti di bawah ini.</p><pre>fun View.visibleIf(condition: Boolean) {<br>    this.visibility = if (condition) View.VISIBLE else View.GONE<br>}</pre><blockquote>Terus, perbedaannya ada di mana bwang?</blockquote><p>Jadi, letak perbedaannya adalah dari cara kita memanggil function tersebut. Kita lihat seperti di bawah ini.</p><pre>private val button: Button = findViewById(R.id.button)<br><br>// Using normal function<br>updateVisibility(button, condition)<br><br>// Using extension function<br>button.visibleIf(condition)</pre><h4>Penutup</h4><p>Jadi, menurut Saya pribadi, memanfaatkan penggunaan <em>extension function </em>dapat meningkatkan <em>readability </em>dari kode kita. Hal ini dapat meningkatkan aura Anda menjadi +99999 dan mendapatkan pujian dari senior Anda, sehingga kelak dapat memiliki gelar “Typing Ganteng” yang sesungguhnya.</p><p>Jika Anda suka dengan konten yang saya tulis, Anda bisa menambah aura dan ke-sigmaan dengan mengikuti saya di beberapa media sosial saya berikut <a href="https://linktr.ee/notsatria">Satria — Android Developer | Instagram, TikTok | Linktree</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0707f2b64720" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>