Tag: mobile

  • 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!