Tag: android

  • Protecting Your Mobile App: Effective Methods to Combat Unauthorized Access

    Introduction: The Digital World’s Hidden Dangers

    Imagine you’re running a popular mobile app that offers rewards to users. Sounds exciting, right? But what if a few clever users find a way to cheat the system for more rewards? This is exactly the challenge many app developers face today.

    In this blog, we’ll describe a real-world story of how we fought back against digital tricksters and protected our app from fraud. It’s like a digital detective story, but instead of solving crimes, we’re stopping online cheaters.

    Understanding How Fraudsters Try to Trick the System

    The Sneaky World of Device Tricks

    Let’s break down how users may try to outsmart mobile apps:

    One way is through device ID manipulation. What is this? Think of a device ID like a unique fingerprint for your phone. Normally, each phone has its own special ID that helps apps recognize it. But some users have found ways to change this ID, kind of like wearing a disguise.

    Real-world example: Imagine you’re at a carnival with a ticket that lets you ride each ride once. A fraudster might try to change their appearance to get multiple rides. In the digital world, changing a device ID is similar—it lets users create multiple accounts and get more rewards than they should.

    How Do People Create Fake Accounts?

    Users have become super creative in making multiple accounts:

    • Using special apps that create virtual phone environments
    • Playing with email addresses
    • Using temporary email services

    A simple analogy: It’s like someone trying to enter a party multiple times by wearing different costumes and using slightly different names. The goal? To get more free snacks or entry benefits.

    The Detective Work: How to Catch These Digital Tricksters

    Tracking User Behavior

    Modern tracking tools are like having a super-smart security camera that doesn’t just record but actually understands what’s happening. Here are some powerful tools you can explore:

    LogRocket: Your App’s Instant Replay Detective

    LogRocket records and replays user sessions, capturing every interaction, error, and performance hiccup. It’s like having a video camera inside your app, helping developers understand exactly what users experience in real time.

    Quick snapshot:

    • Captures user interactions
    • Tracks performance issues
    • Provides detailed session replays
    • Helps identify and fix bugs instantly

    Mixpanel: The User Behavior Analyst

    Mixpanel is a smart analytics platform that breaks down user behavior, tracking how people use your app, where they drop off, and what features they love most. It’s like having a digital detective who understands your users’ journey.

    Key capabilities:

    • Tracks user actions
    • Creates behavior segments
    • Measures conversion rates
    • Provides actionable insights

    What They Do:

    • Notice unusual account creation patterns
    • Detect suspicious activities
    • Prevent potential fraud before it happens

    Email Validation: The First Line of Defense

    How it works:

    • Recognize similar email addresses
    • Prevent creating multiple accounts with slightly different emails
    • Block tricks like:
      • a.bhi629@gmail.com
      • abhi.629@gmail.com

    Real-life comparison: It’s like a smart mailroom that knows “John Smith” and “J. Smith” are the same person, preventing duplicate mail deliveries.

    Advanced Protection Strategies

    Device ID Tracking

    Key Functions:

    • Store unique device information
    • Check if a device has already claimed rewards
    • Prevent repeat bonus claims

    Simple explanation: Imagine a bouncer at a club who remembers everyone who’s already entered and stops them from sneaking in again.

    Stopping Fake Device Environments

    Some users try to create fake device environments using apps like:

    • Parallel Space
    • Multiple account creators
    • Game cloners

    Protection method: The app identifies and blocks these applications, just like a security system that recognizes fake ID cards.

    Root Device Detection

    What is a Rooted Device? It’s like a phone that’s been modified to give users complete control, bypassing normal security restrictions.

    Detection techniques:

    • Check for special root access files
    • Verify device storage
    • Run specific detection commands

    Analogy: It’s similar to checking if a car has been illegally modified to bypass speed limits.

    Extra Security Layers

    Android Version Requirements

    Upgrading to newer Android versions provides additional security:

    • Better detection of modified devices
    • Stronger app protection
    • More restricted file access

    Simple explanation: It’s like upgrading your home’s security system to a more advanced model that can detect intruders more effectively.

    Additional Protection Methods

    • Data encryption
    • Secure internet communication
    • Location verification
    • Encrypted local storage

    Think of these as multiple locks on your digital front door, each providing an extra layer of protection.

    Real-World Implementation Challenges

    Why is This Important?

    Every time a fraudster successfully tricks the system:

    • The app loses money
    • Genuine users get frustrated
    • Trust in the platform decreases

    Business impact: Imagine running a loyalty program where some people find ways to get 10 times more rewards than others. Not fair, right?

    Practical Tips for App Developers

    • Always stay updated with the latest security trends
    • Regularly audit your app’s security
    • Use multiple protection layers
    • Be proactive, not reactive
    • Learn from each attempted fraud

    Common Misconceptions About App Security

    Myth: “My small app doesn’t need advanced security.” Reality: Every app, regardless of size, can be a target.

    Myth: “Security is a one-time setup.” Reality: Security is an ongoing process of learning and adapting.

    Learning from Real Experiences

    These examples come from actual developers at Velotio Technologies, who faced these challenges head-on. Their approach wasn’t about creating an unbreakable system but about making fraud increasingly difficult and expensive.

    The Human Side of Technology

    Behind every security feature is a human story:

    • Developers protecting user experiences
    • Companies maintaining trust
    • Users expecting fair treatment

    Looking to the Future

    Technology will continue evolving, and so, too, will fraud techniques. The key is to:

    • Stay curious
    • Keep learning
    • Never assume you know everything

    Final Thoughts: Your App, Your Responsibility

    Protecting your mobile app isn’t just about implementing complex technical solutions; it’s about a holistic approach that encompasses understanding user behavior, creating fair experiences, and building trust. Here’s a deeper look into these critical aspects:

    Understanding User Behavior:‍

    Understanding how users interact with your app is crucial. By analyzing user behavior, you can identify patterns that may indicate fraudulent activity. For instance, if a user suddenly starts claiming rewards at an unusually high rate, it could signal potential abuse.
    Utilize analytics tools to gather data on user interactions. This data can help you refine your app’s design and functionality, ensuring it meets genuine user needs while also being resilient against misuse.

    Creating Fair Experiences:‍

    Clearly communicate your app’s rewards, account creation, and user behavior policies. Transparency helps users understand the rules and reduces the likelihood of attempts to game the system.
    Consider implementing a user agreement that outlines acceptable behavior and the consequences of fraudulent actions.

    Building Trust:

    Maintain open lines of communication with your users. Regular updates about security measures, app improvements, and user feedback can help build trust and loyalty.
    Use newsletters, social media, and in-app notifications to keep users informed about changes and enhancements.
    Provide responsive customer support to address user concerns promptly. If users feel heard and valued, they are less likely to engage in fraudulent behavior.

    Implement a robust support system that allows users to report suspicious activities easily and receive timely assistance.

    Remember: Every small protection measure counts.

    Call to Action

    Are you an app developer? Start reviewing your app’s security today. Don’t wait for a fraud incident to take action.

    Want to learn more?

    • Follow security blogs
    • Attend tech conferences
    • Connect with security experts
    • Never stop learning
  • React Native: Session Reply with Microsoft Clarity

    Microsoft recently launched session replay support for iOS on both Native iOS and React Native applications. We decided to see how it performs compared to competitors like LogRocket and UXCam.

    This blog discusses what session replay is, how it works, and its benefits for debugging applications and understanding user behavior. We will also quickly integrate Microsoft Clarity in React Native applications and compare its performance with competitors like LogRocket and UXCam.

    Below, we will explore the key features of session replay, the steps to integrate Microsoft Clarity into your React Native application, and benchmark its performance against other popular tools.

    Key Features of Session Replay

    Session replay provides a visual playback of user interactions on your application. This allows developers to observe how users navigate the app, identify any issues they encounter, and understand user behavior patterns. Here are some of the standout features:

    • User Interaction Tracking: Record clicks, scrolls, and navigation paths for a comprehensive view of user activities.
    • Error Monitoring: Capture and analyze errors in real time to quickly diagnose and fix issues.
    • Heatmaps: Visualize areas of high interaction to understand which parts of the app are most engaging.
    • Anonymized Data: Ensure user privacy by anonymizing sensitive information during session recording.

    Integrating Microsoft Clarity with React Native

    Integrating Microsoft Clarity into your React Native application is a straightforward process. Follow these steps to get started:

    1. Sign Up for Microsoft Clarity:

    a. Visit the Microsoft Clarity website and sign up for a free account.

    b. Create a new project and obtain your Clarity tracking code.

    1. Install the Clarity SDK:

    Use npm or yarn to install the Clarity SDK in your React Native project:

    npm install clarity@latest‍ 
    yarn add clarity@latest

    1. Initialize Clarity in Your App:

    Import and initialize Clarity in your main application file (e.g., App.js):

    import Clarity from 'clarity';‍
    Clarity.initialize('YOUR_CLARITY_TRACKING_CODE');

    1. Verify Integration:

    a. Run your application and navigate through various screens to ensure Clarity is capturing session data correctly.

    b. Log into your Clarity dashboard to see the recorded sessions and analytics.

    Benchmarking Against Competitors

    To evaluate the performance of Microsoft Clarity, we’ll compare it against two popular session replay tools, LogRocket and UXCam, assessing them based on the following criteria:

    • Ease of Integration: How simple is integrating the tool into a React Native application?
    • Feature Set: What features does each tool offer for session replay and user behavior analysis?
    • Performance Impact: How does the tool impact the app’s performance and user experience?
    • Cost: What are the pricing models and how do they compare?

    Detailed Comparison

    Ease of Integration

    • Microsoft Clarity: The integration process is straightforward and well-documented, making it easy for developers to get started.
    • LogRocket: LogRocket also offers a simple integration process with comprehensive documentation and support.
    • UXCam: UXCam provides detailed guides and support for integration, but it may require additional configuration steps compared to Clarity and LogRocket.

    Feature Set

    • Microsoft Clarity: Offers robust session replay, heatmaps, and error monitoring. However, it may lack some advanced features found in premium tools.
    • LogRocket: Provides a rich set of features, including session replay, performance monitoring, Network request logs, and integration with other tools like Redux and GraphQL.
    • UXCam: Focuses on mobile app analytics with features like session replay, screen flow analysis, and retention tracking.

    Performance Impact

    • Microsoft Clarity: Minimal impact on app performance, making it a suitable choice for most applications.
    • LogRocket: Slightly heavier than Clarity but offers more advanced features. Performance impact is manageable with proper configuration.
    • UXCam: Designed for mobile apps with performance optimization in mind. The impact is generally low but can vary based on app complexity.

    Cost

    • Microsoft Clarity: Free to use, making it an excellent option for startups and small teams.
    • LogRocket: Offers tiered pricing plans, with a free tier for basic usage and paid plans for advanced features.
    • UXCam: Provides a range of pricing options, including a free tier. Paid plans offer more advanced features and higher data limits.

    Final Verdict

    After evaluating the key aspects of session replay tools, Microsoft Clarity stands out as a strong contender, especially for teams looking for a cost-effective solution with essential features. LogRocket and UXCam offer more advanced capabilities, which may be beneficial for larger teams or more complex applications.

    Ultimately, the right tool will depend on your specific needs and budget. For basic session replay and user behavior insights, Microsoft Clarity is a fantastic choice. If you require more comprehensive analytics and integrations, LogRocket or UXCam may be worth the investment.

    Sample App

    I have also created a basic sample app to demonstrate how to set up Microsoft Clarity for React Native apps.

    Please check it out here: https://github.com/rakesho-vel/ms-rn-clarity-sample-app

    This sample video shows how Microsoft Clarity records and lets you review user sessions on its dashboard.

    References

    1. https://clarity.microsoft.com/blog/clarity-sdk-release/
    2. https://web.swipeinsight.app/posts/microsoft-clarity-finally-launches-ios-sdk-8312

  • Revolutionizing Android UI with MotionLayout: A Beginner’s Guide

    In the ever-evolving world of Android app development, seamless integration of compelling animations is key to a polished user experience. MotionLayout, a robust tool in the Android toolkit, has an effortless and elegant ability to embed animations directly into the UI. Join us as we navigate through its features and master the skill of effortlessly designing stunning visuals.

    1. Introduction to MotionLayout

    MotionLayout transcends conventional layouts, standing as a specialized tool to seamlessly synchronize a myriad of animations with screen updates in your Android application.

    1.1 Advantages of MotionLayout

    Animation Separation:

    MotionLayout distinguishes itself with the ability to compartmentalize animation logic into a separate XML file. This not only optimizes Java or Kotlin code but also enhances its overall manageability.

    No Dependence on Manager or Controller:

    An exceptional feature of MotionLayout is its user-friendly approach, enabling developers to attach intricate animations to screen changes without requiring a dedicated animation manager or controller.

    Backward Compatibility:

    Of paramount importance, MotionLayout maintains backward compatibility, ensuring its applicability across Android systems starting from API level 14.

    Android Studio Integration:

    Empowering developers further is the seamless integration with Android Studio. The graphical tooling provided by the visual editor facilitates the design and fine-tuning of MotionLayout animations, offering an intuitive workflow.

    Derivation from ConstraintLayout:

    MotionLayout, being a subclass of ConstraintLayout, serves as an extension specifically designed to facilitate the implementation of complex motion and animation design within a ConstraintLayout.

    1.2 Important Tags

    As elucidated earlier, Animation XML is separated into the following important tags and attributes:

    <MotionScene>: The topmost tag in XML, wrapping all subsequent tags.

    <ConstraintSet>: Describes one screen state, with two sets required for animations between states. For example, if we desire an animation where the screen transitions from state A to state B, we necessitate the definition of two ConstraintSets.

    <Transition>: Attaches to two ConstraintSets, triggering animation between them.

    <ViewTransition>: Utilized for changes within a single ConstraintSet.

    As explained before, animation XML is separate following are important tags and attributes that we should know

    1.3 Why It’s Better Than Its Alternatives

    It’s important to note that MotionLayout is not the sole solution for every animation scenario. Similar to the saying that a sword cannot replace a needle, MotionLayout can be a better solution when planning for complex animations. MotionLayout can replace animation created using threads and runnables. Apart from MotionLayout, several common alternatives for creating animations include:

    • Animated Vector Drawable
    • Property animation frameworks
    • LayoutTransition animation
    • Layout Transitions with TransitionManager
    • CoordinatorLayout

    Each alternative has unique advantages and disadvantages compared to MotionLayout. For smaller animations like icon changes, Animated Vector Drawable might be preferred. The choice between alternatives depends on the specific requirements of the animation task at hand.

    MotionLayout is a comprehensive solution, bridging the gap between layout transitions and complex motion handling. It seamlessly integrates features from the property animation framework, TransitionManager, and CoordinatorLayout. Developers can describe transitions between layouts, animate any property, handle touch interactions, and achieve a fully declarative implementation, all through the expressive power of XML.

    2. Configuration

    2.1 System setup

    For optimal development and utilization of the Motion Editor, Android Studio is a prerequisite. Kindly follow this link for the Android Studio installation guide.

    2.2 Project Implementation

    1. Initiate a new Android project and opt for the “Empty View Activity” template.

    1. Since MotionLayout is an extension of ConstraintLayout, it’s essential to include ConstraintLayout in the build.gradle file.

    implementation ‘androidx.constraintlayout:constraintlayout:x.x.x’

    Substitute “x.x.x” with the most recent version of ConstraintLayout.

    1. Replace “ConstraintLayout” with “MotionLayout.” Opting for the right-click method is recommended, as it facilitates automatically creating the necessary animation XML.
    Figure 1

    When converting our existing layout to MotionLayout by right-clicking, a new XML file named “activity_main_scene.xml” is generated in the XML directory. This file is dedicated to storing animation details for MotionLayout.

    1. Execute the following steps:
    1. Click on the “start” ConstraintSet.
    2. Move the Text View by dragging it to a desired position on your screen.
    3. Click on the “end” ConstraintSet.
    4. Move the Text View to another position on your screen.
    5. Click on the arrow above “start” and “end” ConstraintSet.
    6. Click on the “+” symbol in the “Attributes” tab.
    7. Add the attribute “autoTransition” with the value “jumpToEnd.”
    8. Click the play button on the “Transition” tab.

    Preview the animation in real time by running the application. The animation will initiate when called from the associated Java class.

    Note: You can also manually edit the activity_main_scene.xml file to make these changes.

    3. Sample Project and Result

    Until now, we’ve navigated through the complexities of MotionLayout and laid the groundwork for an Android project. Now, let’s transition from theory to practical application by crafting a sample project. In this endeavor, we’ll keep the animation simple and accessible for a clearer understanding.

    3.1 Adding Dependencies

    Include the following lines of code in your gradle.build file (Module: app), and then click on “Sync Now” to ensure synchronization with the project:

    android {
       buildFeatures {
           viewBinding true
       }
    }
    
    dependencies {
       implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
       implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
       implementation 'androidx.annotation:annotation:1.5.0'
    }

    3.2 Adding code

    Include the following code snippets in their corresponding classes:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:background="@android:color/darker_gray"
       app:layoutDescription="@xml/activity_main_scene"
       android:id="@+id/layoutMain"
       tools:context=".MainActivity">
    
       <ImageView
           android:id="@+id/image_view_00"
           android:layout_width="80dp"
           android:layout_height="100dp"
           android:layout_margin="32dp"
           android:src="@drawable/card_back"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintTop_toTopOf="parent" />
    
       <ImageView
           android:id="@+id/image_view_01"
           android:layout_width="80dp"
           android:layout_height="100dp"
           android:layout_margin="32dp"
           android:src="@drawable/card_back"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintTop_toTopOf="parent" />
    
       <ImageView
           android:id="@+id/image_view_02"
           android:layout_width="80dp"
           android:layout_height="100dp"
           android:layout_margin="32dp"
           android:src="@drawable/card_back"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintTop_toTopOf="parent" />
    
    </androidx.constraintlayout.motion.widget.MotionLayout>

    <?xml version="1.0" encoding="utf-8"?>
    <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:motion="http://schemas.android.com/apk/res-auto">
    
       <ConstraintSet android:id="@+id/image_00">
           <Constraint
               android:id="@+id/image_view_00"
               android:layout_width="80dp"
               android:layout_height="100dp"
               android:layout_marginTop="32dp"
               motion:layout_constraintEnd_toEndOf="parent"
               motion:layout_constraintStart_toStartOf="parent"
               motion:layout_constraintTop_toTopOf="parent" />
           <Constraint
               android:id="@+id/image_view_01"
               android:layout_width="80dp"
               android:layout_height="100dp"
               android:layout_marginTop="32dp"
               motion:layout_constraintEnd_toEndOf="parent"
               motion:layout_constraintStart_toStartOf="parent"
               motion:layout_constraintTop_toTopOf="parent" />
           <Constraint
               android:id="@+id/image_view_02"
               android:layout_width="80dp"
               android:layout_height="100dp"
               android:layout_marginTop="32dp"
               motion:layout_constraintEnd_toEndOf="parent"
               motion:layout_constraintStart_toStartOf="parent"
               motion:layout_constraintTop_toTopOf="parent" />
       </ConstraintSet>
       <ConstraintSet android:id="@+id/image_01">
           <Constraint
               android:id="@+id/image_view_00"
               android:layout_width="80dp"
               android:layout_height="100dp"
               android:layout_marginBottom="32dp"
               android:layout_marginEnd="44dp"
               android:src="@drawable/card_back"
               motion:layout_constraintBottom_toBottomOf="parent"
               motion:layout_constraintEnd_toEndOf="@id/image_view_01" />
           <Constraint
               android:id="@+id/image_view_01"
               android:layout_width="80dp"
               android:layout_height="100dp"
               android:layout_marginBottom="32dp"
               android:src="@drawable/card_back"
               motion:layout_constraintBottom_toBottomOf="parent"
               motion:layout_constraintEnd_toEndOf="parent"
               motion:layout_constraintStart_toStartOf="parent" />
           <Constraint
               android:id="@+id/image_view_02"
               android:layout_width="80dp"
               android:layout_height="100dp"
               android:layout_marginBottom="32dp"
               android:layout_marginStart="44dp"
               android:src="@drawable/card_back"
               motion:layout_constraintBottom_toBottomOf="parent"
               motion:layout_constraintStart_toStartOf="@id/image_view_01" />
       </ConstraintSet>
    
       <Transition
           motion:autoTransition="animateToEnd"
           motion:constraintSetEnd="@+id/image_01"
           motion:constraintSetStart="@id/image_00"
           motion:duration="5000" />
    </MotionScene>

    class MainActivity : AppCompatActivity() {
       private lateinit var binding: ActivityMainBinding
       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContentView(R.layout.activity_main)
           binding = ActivityMainBinding.inflate(layoutInflater)
           setContentView(binding.root)
           binding.apply {
               lifecycleScope.launch {
                   delay(1000)
                   layoutMain.transitionToEnd()
               }
           }
       }
    }

    3.3 Result

    For a thorough comprehension of the implementation specifics and complete access to the source code, allowing you to delve into the intricacies of the project and harness its functionalities adeptly, please refer to this repository.

    4. Assignment

    Expanding the animation’s complexity becomes seamless by incorporating additional elements with meticulous handling. Here’s an assignment for you: endeavor to create the specified output below.

    4.1 Assignment 1

    4.2 Assignment 2

    5. Conclusion

    In conclusion, this guide has explored the essentials of using MotionLayout in Android development, highlighting its superiority over other animation methods. While we’ve touched on its basic capabilities here, future installments will explore more advanced features and uses. We hope this piece has ignited your interest in MotionLayout’s potential to enhance your Android apps.

    Thank you for dedicating your time to this informative read!

    6. References

    1. https://developer.android.com/reference/androidx/constraintlayout/motion/widget/MotionLayout
    2. https://developer.android.com/reference/androidx/constraintlayout/widget/ConstraintLayout
    3. https://android-developers.googleblog.com/2021/02/mad-skills-motion-layout-wrap-up.html
    4. https://www.bacancytechnology.com/blog/motionlayout-in-android
    5. https://medium.com/mindful-engineering/getting-started-with-motion-layout-in-android-c52af5d5076c
    6. https://blog.mindorks.com/getting-started-with-motion-layout-android-tutorials/
    7. https://gorillalogic.com/blog/a-motionlayout-tutorial-create-motions-and-animations-for-android
    8. https://taglineinfotech.com/motion-layout-in-android/
    9. https://applover.com/blog/how-to-create-motionlayout-part1/
    10. https://medium.com/simform-engineering/animations-in-android-featuring-motionlayout-from-scratch-3ec5cbd6b616
    11. https://www.nomtek.com/blog/motionlayout
  • Discover the Benefits of Android Clean Architecture

    All architectures have one common goal: to manage the complexity of our application. We may not need to worry about it on a smaller project, but it becomes a lifesaver on larger ones. The purpose of Clean Architecture is to minimize code complexity by preventing implementation complexity.

    We must first understand a few things to implement the Clean Architecture in an Android project.

    • Entities: Encapsulate enterprise-wide critical business rules. An entity can be an object with methods or data structures and functions.
    • Use cases: It demonstrates data flow to and from the entities.
    • Controllers, gateways, presenters: A set of adapters that convert data from the use cases and entities format to the most convenient way to pass the data to the upper level (typically the UI).
    • UI, external interfaces, DB, web, devices: The outermost layer of the architecture, generally composed of frameworks such as database and web frameworks.

    Here is one thumb rule we need to follow. First, look at the direction of the arrows in the diagram. Entities do not depend on use cases and use cases do not depend on controllers, and so on. A lower-level module should always rely on something other than a higher-level module. The dependencies between the layers must be inwards.

    Advantages of Clean Architecture:

    • Strict architecture—hard to make mistakes
    • Business logic is encapsulated, easy to use, and tested
    • Enforcement of dependencies through encapsulation
    • Allows for parallel development
    • Highly scalable
    • Easy to understand and maintain
    • Testing is facilitated

    Let’s understand this using the small case study of the Android project, which gives more practical knowledge rather than theoretical.

    A pragmatic approach

    A typical Android project typically needs to separate the concerns between the UI, the business logic, and the data model, so taking “the theory” into account, we decided to split the project into three modules:

    • Domain Layer: contains the definitions of the business logic of the app, the data models, the abstract definition of repositories, and the definition of the use cases.
    Domain Module
    • Data Layer: This layer provides the abstract definition of all the data sources. Any application can reuse this without modifications. It contains repositories and data sources implementations, the database definition and its DAOs, the network APIs definitions, some mappers to convert network API models to database models, and vice versa.
    Data Module
    • Presentation layer: This is the layer that mainly interacts with the UI. It’s Android-specific and contains fragments, view models, adapters, activities, composable, and so on. It also includes a service locator to manage dependencies.
    Presentation Module

    Marvel’s comic characters App

    To elaborate on all the above concepts related to Clean Architecture, we are creating an app that lists Marvel’s comic characters using Marvel’s developer API. The app shows a list of Marvel characters, and clicking on each character will show details of that character. Users can also bookmark their favorite characters. It seems like nothing complicated, right?

    Before proceeding further into the sample, it’s good to have an idea of the following frameworks because the example is wholly based on them.

    • Jetpack Compose – Android’s recommended modern toolkit for building native UI.
    • Retrofit 2 – A type-safe HTTP client for Android for Network calls.
    • ViewModel – A class responsible for preparing and managing the data for an activity or a fragment.
    • Kotlin – Kotlin is a cross-platform, statically typed, general-purpose programming language with type inference.

    To get a characters list, we have used marvel’s developer API, which returns the list of marvel characters.

    http://gateway.marvel.com/v1/public/characters

    The domain layer

    In the domain layer, we define the data model, the use cases, and the abstract definition of the character repository. The API returns a list of characters, with some info like name, description, and image links.

    data class CharacterEntity(
        val id: Long,
        val name: String,
        val description: String,
        val imageUrl: String,
        val bookmarkStatus: Boolean
    )

    interface MarvelDataRepository {
        suspend fun getCharacters(dataSource: DataSource): Flow<List<CharacterEntity>>
        suspend fun getCharacter(characterId: Long): Flow<CharacterEntity>
        suspend fun toggleCharacterBookmarkStatus(characterId: Long): Boolean
        suspend fun getComics(dataSource: DataSource, characterId: Long): Flow<List<ComicsEntity>>
    }

    class GetCharactersUseCase(
        private val marvelDataRepository: MarvelDataRepository,
        private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
    ) {
        operator fun invoke(forceRefresh: Boolean = false): Flow<List<CharacterEntity>> {
            return flow {
                emitAll(
                    marvelDataRepository.getCharacters(
                        if (forceRefresh) {
                            DataSource.Network
                        } else {
                            DataSource.Cache
                        }
                    )
                )
            }
                .flowOn(ioDispatcher)
        }
    }

    The data layer

    As we said before, the data layer must implement the abstract definition of the domain layer, so we need to put the repository’s concrete implementation in this layer. To do so, we can define two data sources, a “local” data source to provide persistence and a “remote” data source to fetch the data from the API.

    class MarvelDataRepositoryImpl(
        private val marvelRemoteService: MarvelRemoteService,
        private val charactersDao: CharactersDao,
        private val comicsDao: ComicsDao,
        private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
    ) : MarvelDataRepository {
    
        override suspend fun getCharacters(dataSource: DataSource): Flow<List<CharacterEntity>> =
            flow {
                emitAll(
                    when (dataSource) {
                        is DataSource.Cache -> getCharactersCache().map { list ->
                            if (list.isEmpty()) {
                                getCharactersNetwork()
                            } else {
                                list.toDomain()
                            }
                        }
                            .flowOn(ioDispatcher)
    
                        is DataSource.Network -> flowOf(getCharactersNetwork())
                            .flowOn(ioDispatcher)
                    }
                )
            }
    
        private suspend fun getCharactersNetwork(): List<CharacterEntity> =
            marvelRemoteService.getCharacters().body()?.data?.results?.let { remoteData ->
                if (remoteData.isNotEmpty()) {
                    charactersDao.upsert(remoteData.toCache())
                }
                remoteData.toDomain()
            } ?: emptyList()
    
        private fun getCharactersCache(): Flow<List<CharacterCache>> =
            charactersDao.getCharacters()
    
        override suspend fun getCharacter(characterId: Long): Flow<CharacterEntity> =
            charactersDao.getCharacterFlow(id = characterId).map {
                it.toDomain()
            }
    
        override suspend fun toggleCharacterBookmarkStatus(characterId: Long): Boolean {
    
            val status = charactersDao.getCharacter(characterId)?.bookmarkStatus?.not() ?: false
    
            return charactersDao.toggleCharacterBookmarkStatus(id = characterId, status = status) > 0
        }
    
        override suspend fun getComics(
            dataSource: DataSource,
            characterId: Long
        ): Flow<List<ComicsEntity>> = flow {
            emitAll(
                when (dataSource) {
                    is DataSource.Cache -> getComicsCache(characterId = characterId).map { list ->
                        if (list.isEmpty()) {
                            getComicsNetwork(characterId = characterId)
                        } else {
                            list.toDomain()
                        }
                    }
                    is DataSource.Network -> flowOf(getComicsNetwork(characterId = characterId))
                        .flowOn(ioDispatcher)
                }
            )
        }
    
        private suspend fun getComicsNetwork(characterId: Long): List<ComicsEntity> =
            marvelRemoteService.getComics(characterId = characterId)
                .body()?.data?.results?.let { remoteData ->
                    if (remoteData.isNotEmpty()) {
                        comicsDao.upsert(remoteData.toCache(characterId = characterId))
                    }
                    remoteData.toDomain()
                } ?: emptyList()
    
        private fun getComicsCache(characterId: Long): Flow<List<ComicsCache>> =
            comicsDao.getComics(characterId = characterId)
    }

    Since we defined the data source to manage persistence, in this layer, we also need to determine the database for which we are using the room database. In addition, it’s good practice to create some mappers to map the API response to the corresponding database entity.

    fun List<Characters>.toCache() = map { character -> character.toCache() }
    
    fun Characters.toCache() = CharacterCache(
        id = id ?: 0,
        name = name ?: "",
        description = description ?: "",
        imageUrl = thumbnail?.let {
            "${it.path}.${it.extension}"
        } ?: ""
    )
    
    fun List<Characters>.toDomain() = map { character -> character.toDomain() }
    
    fun Characters.toDomain() = CharacterEntity(
        id = id ?: 0,
        name = name ?: "",
        description = description ?: "",
        imageUrl = thumbnail?.let {
            "${it.path}.${it.extension}"
        } ?: "",
        bookmarkStatus = false
    )

    @Entity
    data class CharacterCache(
        @PrimaryKey
        val id: Long,
        val name: String,
        val description: String,
        val imageUrl: String,
        val bookmarkStatus: Boolean = false
    ) : BaseCache

    The presentation layer

    In this layer, we need a UI component like fragments, activity, or composable to display the list of characters; here, we can use the widely used MVVM approach. The view model takes the use cases in its constructors and invokes the corresponding use case according to user actions (get a character, characters & comics, etc.).

    Each use case will invoke the appropriate method in the repository.

    class CharactersListViewModel(
        private val getCharacters: GetCharactersUseCase,
        private val toggleCharacterBookmarkStatus: ToggleCharacterBookmarkStatus
    ) : ViewModel() {
    
        private val _characters = MutableStateFlow<UiState<List<CharacterViewState>>>(UiState.Loading())
        val characters: StateFlow<UiState<List<CharacterViewState>>> = _characters
    
        init {
            _characters.value = UiState.Loading()
            getAllCharacters()
        }
    
        private fun getAllCharacters(forceRefresh: Boolean = false) {
            getCharacters(forceRefresh)
                .catch { error ->
                    error.printStackTrace()
                    when (error) {
                        is UnknownHostException, is ConnectException, is SocketTimeoutException -> _characters.value =
                            UiState.NoInternetError(error)
                        else -> _characters.value = UiState.ApiError(error)
                    }
                }.map { list ->
                    _characters.value = UiState.Loaded(list.toViewState())
                }.launchIn(viewModelScope)
        }
    
        fun refresh(showLoader: Boolean = false) {
            if (showLoader) {
                _characters.value = UiState.Loading()
            }
            getAllCharacters(forceRefresh = true)
        }
    
        fun bookmarkCharacter(characterId: Long) {
            viewModelScope.launch {
                toggleCharacterBookmarkStatus(characterId = characterId)
            }
        }
    }

    /*
    * Scaffold(Layout) for Characters list page
    * */
    
    
    @SuppressLint("UnusedMaterialScaffoldPaddingParameter")
    @Composable
    fun CharactersListScaffold(
        showComics: (Long) -> Unit,
        closeAction: () -> Unit,
        modifier: Modifier = Modifier,
        charactersListViewModel: CharactersListViewModel = getViewModel()
    ) {
        Scaffold(
            modifier = modifier,
            topBar = {
                TopAppBar(
                    title = {
                        Text(text = stringResource(id = R.string.characters))
                    },
                    navigationIcon = {
                        IconButton(onClick = closeAction) {
                            Icon(
                                imageVector = Icons.Filled.Close,
                                contentDescription = stringResource(id = R.string.close_icon)
                            )
                        }
                    }
                )
            }
        ) {
            val state = charactersListViewModel.characters.collectAsState()
    
            when (state.value) {
    
                is UiState.Loading -> {
                    Loader()
                }
    
                is UiState.Loaded -> {
                    state.value.data?.let { characters ->
                        val isRefreshing = remember { mutableStateOf(false) }
                        SwipeRefresh(
                            state = rememberSwipeRefreshState(isRefreshing = isRefreshing.value),
                            onRefresh = {
                                isRefreshing.value = true
                                charactersListViewModel.refresh()
                            }
                        ) {
                            isRefreshing.value = false
    
                            if (characters.isNotEmpty()) {
    
                                LazyVerticalGrid(
                                    columns = GridCells.Fixed(2),
                                    modifier = Modifier
                                        .padding(5.dp)
                                        .fillMaxSize()
                                ) {
                                    items(characters) { state ->
                                        CharacterTile(
                                            state = state,
                                            characterSelectAction = {
                                                showComics(state.id)
                                            },
                                            bookmarkAction = {
                                                charactersListViewModel.bookmarkCharacter(state.id)
                                            },
                                            modifier = Modifier
                                                .padding(5.dp)
                                                .fillMaxHeight(fraction = 0.35f)
                                        )
                                    }
                                }
    
                            } else {
                                Info(
                                    messageResource = R.string.no_characters_available,
                                    iconResource = R.drawable.ic_no_data
                                )
                            }
                        }
                    }
                }
    
                is UiState.ApiError -> {
                    Info(
                        messageResource = R.string.api_error,
                        iconResource = R.drawable.ic_something_went_wrong
                    )
                }
    
                is UiState.NoInternetError -> {
                    Info(
                        messageResource = R.string.no_internet,
                        iconResource = R.drawable.ic_no_connection,
                        isInfoOnly = false,
                        buttonAction = {
                            charactersListViewModel.refresh(showLoader = true)
                        }
                    )
                }
            }
        }
    }
    
    @Preview
    @Composable
    private fun CharactersListScaffoldPreview() {
        MarvelComicTheme {
            CharactersListScaffold(showComics = {}, closeAction = {})
        }
    }

    Let’s see how the communication between the layers looks like.

    Source: Clean Architecture Tutorial for Android

    As you can see, each layer communicates only with the closest one, keeping inner layers independent from lower layers, this way, we can quickly test each module separately, and the separation of concerns will help developers to collaborate on the different modules of the project.

    Thank you so much!