Giter Club home page Giter Club logo

android-showcase's Introduction

💎 Android Showcase 2.0

Kotlin Version AGP Gradle

Codebeat Badge CodeFactor

The Android Showcase project exemplifies modern Android application development methodologies and provides comprehensive architectural guidance. By integrating popular development tools, libraries, linters, and Gradle plugins, along with robust testing frameworks and Continuous Integration (CI) setup, this project offers a holistic sample of a fully operational Android application.

The primary focus of this project lies in promoting a modular, scalable, maintainable, and testable architecture. It incorporates a leading-edge tech-stack and embodies the finest practices in software development. While the application may appear straightforward, it encompasses all the crucial components that lay the groundwork for a robust, large-scale application.

The design principles and architectural choices applied in this project are ideally suited for larger teams and extended application lifecycles. This application is not just about showcasing functionalities, but it is also a testament to how well-structured and well-written code serves as a stable backbone for scalable and maintainable software development projects.

Application Scope

The android-showcase is a simple application that presents information about various music albums. This data is dynamically sourced from the Last.fm music platform API.

The app has a few screens located in multiple feature modules:

  • Album list screen - displays list of albums
  • Album detail screen - display information about the selected album
  • Profile screen - empty (WiP)
  • Favourites screen - empty (WiP)

Tech-Stack

This project takes advantage of best practices and many popular libraries and tools in the Android ecosystem. Most of the libraries are in the stable version unless there is a good reason to use non-stable dependency.

Architecture

By dividing a problem into smaller and easier-to-solve sub-problems, we can reduce the complexity of designing and maintaining a large system. Each module is an independent build block serving a clear purpose. We can think about each feature as a reusable component, the equivalent of microservice or private library.

The modularized code-base approach provides a few benefits:

  • reusability - enable code sharing and building multiple apps from the same foundation. Apps should be a sum of their features where the features are organized as separate modules.
  • separation of concerns - each module has a clear API. Feature-related classes live in different modules and can't be referenced without explicit module dependency. We strictly control what is exposed to other parts of your codebase.
  • features can be developed in parallel eg. by different teams
  • each feature can be developed in isolation, independently from other features
  • faster build time

Module Types And Module Dependencies

This diagram presents dependencies between project modules (Gradle sub-projects).

module_dependencies

We have three kinds of modules in the application:

  • app module - this is the main module. It contains code that wires multiple modules together (class, dependency injection setup, NavHostActivity, etc.) and fundamental application configuration (retrofit configuration, required permissions setup, custom Application class, etc.).
  • feature_x modules - the most common type of module containing all code related to a given feature. share some assets or code only between feature modules (currently app has no such modules)
  • feature_base modules that feature modules depend on to share a common code.

Feature Module Structure

Clean Architecture is implemented at the module level - each module contains its own set of Clean Architecture layers:

module_dependencies_layers

Notice that the app module and library_x modules structure differs a bit from the feature module structure.

Each feature module contains non-layer components and 3 layers with a distinct set of responsibilities.

feature_structure

Presentation Layer

This layer is closest to what the user sees on the screen.

The presentation layer mixes MVVM and MVI patterns:

  • MVVM - Jetpack ViewModel is used to encapsulate a common UI state. It exposes the state via observable state holder (Kotlin Flow)
  • MVI - action modifies the common UI state and emits a new state to a view via Kotlin Flow

The common state is a single source of truth for each view. This solution derives from Unidirectional Data Flow and Redux principles.

This approach facilitates the creation of consistent states. The state is collected via collectAsUiStateWithLifecycle method. Flows collection happens in a lifecycle-aware manner, so no resources are wasted.

Stated is annotated with Immutable annotation that is used by Jetpack compose to enable composition optimizations.

Components:

  • View (Fragment) - observes common view state (through Kotlin Flow). Compose transform state (emitted by Kotlin Flow) into application UI Consumes the state and transforms it into application UI (via Jetpack Compose). Pass user interactions to ViewModel. Views are hard to test, so they should be as simple as possible.
  • ViewModel - emits (through Kotlin Flow) view state changes to the view and deals with user interactions (these view models are not simply POJO classes).
  • ViewState - common state for a single view
  • StateTimeTravelDebugger - logs actions and view state transitions to facilitate debugging.
  • NavManager - singleton that facilitates handling all navigation events inside NavHostActivity (instead of separately, inside each view)

Domain Layer

This is the core layer of the application. Notice that the domain layer is independent of any other layers. This allows making domain models and business logic independent from other layers. In other words, changes in other layers will not affect the domain layer eg. changing the database (data layer) or screen UI (presentation layer) ideally will not result in any code change within the domain layer.

Components:

  • UseCase - contains business logic
  • DomainModel - defines the core structure of the data that will be used within the application. This is the source of truth for application data.
  • Repository interface - required to keep the domain layer independent from the data layer (Dependency inversion).

Data Layer

Encapsulates application data. Provides the data to the domain layer eg. retrieves data from the internet and cache the data in disk cache (when the device is offline).

Components:

  • Repository is exposing data to the domain layer. Depending on the application structure and quality of the external API repository can also merge, filter, and transform the data. These operations intend to create a high-quality data source for the domain layer. It is the responsibility of the Repository (one or more) to construct Domain models by reading from the Data Source and accepting Domain models to be written to the Data Source
  • Mapper - maps data model to domain model (to keep domain layer independent from the data layer).

This application has two Data Sources - Retrofit (used for network access) and Room (local storage used to access device persistent memory). These data sources can be treated as an implicit sub-layer. Each data source consists of multiple classes:

  • Retrofit Service - defines a set of API endpoints
  • Retrofit Response Model - definition of the network objects for a given endpoint (top-level model for the data consists of ApiModels)
  • Retrofit Api Data Model - defines the network objects (sub-objects of the Response Model)
  • Room Database - persistence database to store app data
  • Room DAO - interact with the stored data
  • Room Entity - definition of the stored objects

Both Retrofit API Data Models and Room Entities contain annotations, so the given framework understands how to parse the data into objects.

Data Flow

The below diagram presents application data flow when a user interacts with the album list screen:

app_data_flow

Dependency Management

Gradle versions catalog is used as a centralized dependency management third-party dependency coordinates (group, artifact, version) are shared across all modules (Gradle projects and subprojects).

All of the dependencies are stored in the settings.gradle.kts file (default location). Gradle versions catalog consists of a few major sections:

  • [versions] - declare versions that can be referenced by all dependencies
  • [libraries] - declare the aliases to library coordinates
  • [bundles] - declare dependency bundles (groups)
  • [plugins] - declare Gradle plugin dependencies

Each feature module depends on the feature_base module, so dependencies are shared without the need to add them explicitly in each feature module.

The project enables the TYPESAFE_PROJECT_ACCESSORS experimental Gradle feature to generate type-safe accessors to refer other projects.

// Before
implementation(project(":feature_album"))

// After
implementation(projects.featureAlbum)

Logcat debugging

To facilitate debuting project contains logs. You can filter logs to understand app flow. Keywords:

  • onCreate see what Activities and Fragments have been created
  • Action - filter all actions performed on the screens to update the UI
  • Http - debug network requests and responses

CI Pipeline

CI is utilizing GitHub Actions. The complete GitHub Actions config is located in the .github/workflows folder.

Pull Request Verification

Series of workflows run (in parallel) for every opened PR, and after merging PR to the main branch:

  • ./gradlew konsist_test:test --rerun-tasks - checks that source code satisfies Konsist rules
  • ./gradlew lintDebug - checks that source code satisfies Android lint rules
  • ./gradlew detektCheck - checks that sourcecode satisfies detekt rules
  • ./gradlew spotlessCheck - checks that source code satisfies formatting steps.
  • ./gradlew testDebugUnitTest - run unit tests
  • ./gradlew connectedCheck - run UI tests
  • ./gradlew :app:bundleDebug - create an application bundle

The following tasks cab be executed locally to make codebase compliant with the rules:

  • ./gradlew detektApply - applies detekt code formatting rules to sourcecode in-place
  • ./gradlew spotlessApply - applies code formatting steps to the source code in place.

Design Decisions

Read related articles to have a better understanding of underlying design decisions and various trade-offs.

What This Project Does Not Cover?

The interface of the app utilizes some of the modern material design components, however, is deliberately kept simple to focus on application architecture and project config.

Getting Started

There are a few ways to open this project.

Android Studio

  1. Android Studio -> File -> New -> From Version control -> Git
  2. Enter https://github.com/igorwojda/android-showcase.git into URL field and press Clone button

Command-line And Android Studio

  1. Run git clone https://github.com/igorwojda/android-showcase.git command to clone the project
  2. Open Android Studio and select File | Open... from the menu. Select the cloned directory and press Open button

Plugins

It is recommended to install Detekt to Android Studio. To configure the plugin open Android Studio preferences, open Tools, open Detekt and add detekt.yml configuration file.

Upcoming Improvements

This project is under active development and it is being occasionally refined.

Check the list of all upcoming enhancements.

Inspiration

Here are a few additional resources.

Cheatsheet

Android Projects

Other high-quality projects will help you to find solutions that work for your project (random order):

Other

Known Issues

  • In Gradle 8.1 the version catalog type safe API is not available for buildSrc directory, so dependencies and versions have to be retrieved using type unsafe API:
    • plugins are retrieved using string plugin ids
    • versions (kotlinCompilerExtensionVersion) are retrieved using string version names
  • No usages are found for the suspended Kotlin invoke operator (KTIJ-1053)
  • The Material You Dynamic Colors are not correctly applied to Fragment contents (only to Activity)
  • When using FragmentContainerView, NavController fragment can't be retrieved by using findNavController() (ISSUE-142847973, STACKOVERFLOW-59275182)
  • Mockk is unable to mock some methods with implicit continuation parameter in the AlbumListViewModelTest class (Issue-957), , so test in the AlbumDetailViewModelTest was disabled
  • Automatic Kotlin upgrade is disabled in Renovate, because these dependencies have to be updated together with Kotlin: until:
  • Dynamic feature module is not supported by ANDROID_TEST_USES_UNIFIED_TEST_PLATFORM yet.
  • ktlint FileName rule has to be disabled, because it is not compatible with fie contain a single extension ISSUE-1657
  • Delegate import is not provided when a variable has the same name as Delegate (KTIJ-17403)
  • androidx.compose.runtime.getValue and androidx.compose.runtime.setValue imports are can't be resolved automatically - they had to be added manually KTIJ-23200
  • ktlint import-ordering rule conflicts with IDE default formatting rule, so it have to be .editorconfig file. and KTIJ-16847)
  • False positive "Unused symbol" for a custom Android application class referenced in AndroidManifest.xml file (KT-27971)
  • Android lint complains about exceeding access rights to ArchTaskExecutor (Issue 79189568)
  • JUnit 5 does not support tests with suspended modifier (Issue 1914)
  • Custom detekt config is hard to update (Issue 4517)
  • Coil does not provide a way to automatically retry image load, so some images may not be loaded when connection speed is low (Issue 132)
  • buildFeatures and testOptions blocks are incubating and have to be marked as @Suppress ("UnstableApiUsage")

Contribute

This project is being maintained to stay up to date with leading industry standards. Please check the CONTRIBUTING page if you want to help.

Author

Follow me

Follow me

License

MIT License

Copyright (c) 2019 Igor Wojda

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Animations License

Flowing animations are distributed under Creative Commons License 2.0:

android-showcase's People

Contributors

adilbolatkhanov avatar carminelaface avatar dependabot-preview[bot] avatar dependabot[bot] avatar e4basil avatar hardikm9850 avatar igorwojda avatar islamkhsh avatar itomkinas avatar javabbt avatar jmpaya avatar jrodriguezva avatar kts6056 avatar mateuszteteruk avatar narivo avatar omkar-tenkale avatar renovate[bot] avatar sfeatherstone avatar shahwaiz90 avatar soyllamas avatar tapchicoma avatar tomaszrykala avatar tony---zhang avatar yands11 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

android-showcase's Issues

Caused by: java.lang.ClassNotFoundException: Kodein module class not found

com.igorwojda.showcase E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.igorwojda.showcase, PID: 9269
java.lang.ExceptionInInitializerError
at com.igorwojda.showcase.app.ShowcaseApplication$kodein$1.invoke(ShowcaseApplication.kt:25)
at com.igorwojda.showcase.app.ShowcaseApplication$kodein$1.invoke(ShowcaseApplication.kt:20)
at org.kodein.di.internal.KodeinImpl$Companion.newBuilder(KodeinImpl.kt:22)
at org.kodein.di.internal.KodeinImpl$Companion.access$newBuilder(KodeinImpl.kt:21)
at org.kodein.di.internal.KodeinImpl.(KodeinImpl.kt:19)
at org.kodein.di.Kodein$Companion$lazy$1.invoke(Kodein.kt:447)
at org.kodein.di.Kodein$Companion$lazy$1.invoke(Kodein.kt:429)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at org.kodein.di.LazyKodein.getBaseKodein(Unknown Source:2)
at org.kodein.di.LazyKodein.getContainer(lateinit.kt:31)
at org.kodein.di.internal.KodeinMainBuilderImpl.extend(KodeinBuilderImpl.kt:80)
at org.kodein.di.Kodein$MainBuilder$DefaultImpls.extend$default(Kodein.kt:384)
at com.igorwojda.showcase.library.base.presentation.activity.InjectionActivity$kodein$2.invoke(InjectionActivity.kt:24)
at com.igorwojda.showcase.library.base.presentation.activity.InjectionActivity$kodein$2.invoke(InjectionActivity.kt:15)
at org.kodein.di.internal.KodeinImpl$Companion.newBuilder(KodeinImpl.kt:22)
at org.kodein.di.internal.KodeinImpl$Companion.access$newBuilder(KodeinImpl.kt:21)
at org.kodein.di.internal.KodeinImpl.(KodeinImpl.kt:19)
at org.kodein.di.Kodein$Companion.invoke(Kodein.kt:438)
at org.kodein.di.android.RetainedKt$retainedKodein$1.invoke(retained.kt:34)
at org.kodein.di.android.RetainedKt$retainedKodein$1.invoke(Unknown Source:0)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at com.igorwojda.showcase.library.base.presentation.activity.InjectionActivity.getKodein(Unknown Source:2)
at org.kodein.di.KodeinAwareKt$Instance$1.invoke(KodeinAware.kt:176)
at org.kodein.di.KodeinAwareKt$Instance$1.invoke(Unknown Source:4)
at org.kodein.di.KodeinProperty$provideDelegate$1.invoke(properties.kt:42)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at com.igorwojda.showcase.app.presentation.NavHostActivity.getNavManager(Unknown Source:7)
at com.igorwojda.showcase.app.presentation.NavHostActivity.initNavManager(NavHostActivity.kt:30)
at com.igorwojda.showcase.app.presentation.NavHostActivity.onCreate(NavHostActivity.kt:22)
at android.app.Activity.performCreate(Activity.java:7122)
at android.app.Activity.performCreate(Activity.java:7113)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1220)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2965)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3090)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1794)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:192)
at android.app.ActivityThread.main(ActivityThread.java:6866)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:549)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:817)
Caused by: java.lang.ClassNotFoundException: Kodein module class not found com.igorwojda.showcase.feature.album.FeatureKodeinModule
at com.igorwojda.showcase.app.feature.FeatureManager.(FeatureManager.kt:19)
at com.igorwojda.showcase.app.ShowcaseApplication$kodein$1.invoke(ShowcaseApplication.kt:25) 
at com.igorwojda.showcase.app.ShowcaseApplication$kodein$1.invoke(ShowcaseApplication.kt:20) 
at org.kodein.di.internal.KodeinImpl$Companion.newBuilder(KodeinImpl.kt:22) 
at org.kodein.di.internal.KodeinImpl$Companion.access$newBuilder(KodeinImpl.kt:21) 
at org.kodein.di.internal.KodeinImpl.(KodeinImpl.kt:19) 
at org.kodein.di.Kodein$Companion$lazy$1.invoke(Kodein.kt:447) 
at org.kodein.di.Kodein$Companion$lazy$1.invoke(Kodein.kt:429) 
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74) 
at org.kodein.di.LazyKodein.getBaseKodein(Unknown Source:2) 
at org.kodein.di.LazyKodein.getContainer(lateinit.kt:31) 
at org.kodein.di.internal.KodeinMainBuilderImpl.extend(KodeinBuilderImpl.kt:80) 
at org.kodein.di.Kodein$MainBuilder$DefaultImpls.extend$default(Kodein.kt:384) 
at com.igorwojda.showcase.library.base.presentation.activity.InjectionActivity$kodein$2.invoke(InjectionActivity.kt:24) 
at com.igorwojda.showcase.library.base.presentation.activity.InjectionActivity$kodein$2.invoke(InjectionActivity.kt:15) 
at org.kodein.di.internal.KodeinImpl$Companion.newBuilder(KodeinImpl.kt:22) 
at org.kodein.di.internal.KodeinImpl$Companion.access$newBuilder(KodeinImpl.kt:21) 
at org.kodein.di.internal.KodeinImpl.(KodeinImpl.kt:19) 
at org.kodein.di.Kodein$Companion.invoke(Kodein.kt:438) 
at org.kodein.di.android.RetainedKt$retainedKodein$1.invoke(retained.kt:34) 
at org.kodein.di.android.RetainedKt$retainedKodein$1.invoke(Unknown Source:0) 
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74) 
at com.igorwojda.showcase.library.base.presentation.activity.InjectionActivity.getKodein(Unknown Source:2) 
at org.kodein.di.KodeinAwareKt$Instance$1.invoke(KodeinAware.kt:176) 
at org.kodein.di.KodeinAwareKt$Instance$1.invoke(Unknown Source:4) 
at org.kodein.di.KodeinProperty$provideDelegate$1.invoke(properties.kt:42) 
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74) 
at com.igorwojda.showcase.app.presentation.NavHostActivity.getNavManager(Unknown Source:7) 
at com.igorwojda.showcase.app.presentation.NavHostActivity.initNavManager(NavHostActivity.kt:30) 
at com.igorwojda.showcase.app.presentation.NavHostActivity.onCreate(NavHostActivity.kt:22) 
at android.app.Activity.performCreate(Activity.java:7122) 
at android.app.Activity.performCreate(Activity.java:7113) 
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1220) 
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2965) 
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3090) 
at android.app.ActivityThread.-wrap11(Unknown Source:0) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1794) 
at android.os.Handler.dispatchMessage(Handler.java:106) 
at android.os.Looper.loop(Looper.java:192) 
at android.app.ActivityThread.main(ActivityThread.java:6866) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:549) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:817) 

Integrate ArchUnit tests

ArchUnit is a framework that allows to test app architecture. It looks like it's not suited for android, so some research, experimentation and potentially contribution to ArchUnit may be required to make it work with android project.
TNG/ArchUnit#241

Few tests that we could write with ArchUnit

  • rule that protects boundaries between clean architecture layers - make sure that class defined in ...domain layer does not uses any classes from other layers (have imports from other layers).
  • rule that verifies that every androidx.lifecycle.ViewModel child class has ViewModel suffix
  • More Tests inspirations
    ...

ArchUnit User Guide

Testing: Add UI Tests

We could add some UI test to the project (Espresso?)

eg. basic tests like

  • run the app
  • navigate to details of first item
  • navigate to favourites/profile

Gradle: Duplicated `compileOptions` and `kotlinOptions` blocks

compileOptions and kotlinOptions block are duplicated in each module. Ideally this configuration should be applied only once (and work all the times, even for future modules).

compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
    jvmTarget = JavaVersion.VERSION_1_8.toString()
}

Remove Stetho/Flipper linkage from Release build

Stetho/Flipper are great tools for looking inside your Android/iOS app. But they are not tools we should leave in the app when we build a Release. Show through additional modules how to remove Stetho/Flipper on Release builds.

I am happy to raise a PR for this

Navigation stopped working

Seems that the navigation stopped working after #153.
Nothing happens when I click on an album because this check never returns true:

if (navController.currentDestination?.id == currentFragment?.id) {
navController.navigate(it)
}

If I'm not mistaken, currentFragment?.id returns the id of the fragment's container. It's not the same as currentDestination.id, which is specified in navigation graph xml.

Unable to start activity

Unable to start activity ComponentInfo{com.igorwojda.showcase/com.igorwojda.showcase.app.presentation.NavHostActivity}: android.view.InflateException: Binary XML file line #11: Binary XML file line #11: Error inflating class fragment

Dynamic feature modules and navigation component work together

Hello,
thank you for this repo.I would like to ask, when working together with Dynamic Feature modules and Navigation components, I directly configured the Fragment path of Dynamic Feature module in the Navigation. However, when I was running the project, the system told me that I could not find the target.I noticed that your project is also written in the same way, but the runtime is normal. May I ask if it is my configuration error?Now I have changed the configuration consistent with yours, but it still cannot solve this problem. If you have time trouble, you can help me, thank you!

Help on how to add a new module

I think this project is great as a showcase. However, I am having difficulty adding a module. I tried copy/pasting/refactoring an existing module and I also tried the various options under: File > New >New Module. Can you explain how to add a new module or add it to the README? Also, I am willing to contribute this knowledge to the README if I could figure it out.

Detekt: Check `buildSrc` folder

By default detekt does not check kotlin files within buildSrc folder.

Theres is existing feature request for detekt, to support it out of the box, but for now we need a custom config:
detekt/detekt#1950 (comment)

BTW
Testing - just break code formating in any of the kotlin files located in the buildSrc\src\main\kotlin folder (eg. add multiple empty lines at the end of file or redundant spaces) - make PR. If checks fails - it means that detekt checks buildSrc folder.

Migrate project to Gradle 6 - acessing the buildSrc project and its dependencies

Hi @igorwojda thanks for this project, its super awesome and have used your approach a couple of times on my projects while setting up modules and more so i particularly like your Kotlin Gradle DSL Setup, couldn't find any other complete sample.

However i think you should check on accessing the buildSrc project and its dependencies in settings scripts as it has been deprecated in Gradle v 6.0.

Am currently looking on a work around for the same. In case i find a workaround before you update, will definitely create a PR for this :]

Screenshot from 2020-05-22 13-39-13

Multiple LiveData same viewmodel

Hi Igor, I am a follower of your best practices and I am trying to implement them in my own projects.

My problem is that I have to make 6-8 request in the same view. At the begining I was using the same stateLiveData to display all the lists but it was not working really well and now I have created multiple Observables in my viewmodel but I cannot use the same BaseViewModel as I have to add differents ViewActions and ViewStates as type arguments, also 400 new lines of code spread between classes.

So my question is: Do you have an example on how to do this? Is there a clean solution? My app is working well this way but I feel myself that I am desecrating your code.

Edit: I am using the same instance of stateTimeTravellerDebugger in BaseViewAction. When I have many ViewStates and ViewActions, I have noticed that sometimes when the request are arriving at the same time, stateTimetravellerDebbuger lastViewAction is null. Maybe what I am trying to do is not the best, should I create one logger for any ViewState I have?.

Many Thanks.

how to navigate from featureA to featureB without BottomNavController?

1.how to navigate from featureA to start of featureB without BottomNavController?
if i try navigate from graphFeatureA to graphFeatureB as action, i got a "destination not found exception"
2. how to navigate from featureA to featureB any Fragment?
3. Can i use safeargs in both scenario?

看不懂怎么办

感觉很厉害的样子 但是我看不懂怎么办?有没有中文介绍?

Duplicate plugin id's?

Hello,
thank you for this repo. Why do you have duplicate plugin id's? Specifically in settings.gradle.kts and in build.gradle.kts:
plugins { id(GradlePluginId.DETEKT) version GradlePluginVersion.DETEKT

The application could not be installed: INSTALL_PARSE_FAILED_MANIFEST_EMPTY

I clone the repo and try to run it then it shows

Installation did not succeed.
The application could not be installed: INSTALL_PARSE_FAILED_MANIFEST_EMPTY
List of apks:
[0] 'D:\AndroidStudioProjects\SampleKotlinBase\android-showcase\app\build\outputs\apk\debug\app-debug.apk'
[1] 'D:\AndroidStudioProjects\SampleKotlinBase\android-showcase\feature_album\build\outputs\apk\debug\feature_album-debug.apk'
[2] 'D:\AndroidStudioProjects\SampleKotlinBase\android-showcase\feature_favourite\build\outputs\apk\debug\feature_favourite-debug.apk'
[3] 'D:\AndroidStudioProjects\SampleKotlinBase\android-showcase\feature_profile\build\outputs\apk\debug\feature_profile-debug.apk'
Installation failed due to: 'null'

may be I miss something.
Android Studio 4.

Bottom navigation reload every click

The bottom navigation keep reload the fragments on each click, even if it is the same page.

For example, when I'm on home page and I click the home button, the page reload again.

No need for the staticCheck

Do you really need the staticCheck task in your gradle conf?
Couldn't you just do "./gradlew check" to perform all the tasks related to tests and deteckt/ktlint/etc.?

Detekt: Add Custom `detekt` rule

We could write custom detekt rule.

It can be any rule, but one example that comes to my head is rule that verifies clean architecture layers (verify dependency rule correctness) - make sure that class defined in ...domain layer does not uses any classes from other layers (have imports from other layers).

Another idea would be the rule to Verify that every class that extends androidx.lifecycle.ViewModel class has ViewModel suffix

Also maybe we could also add tests for this rules 🤔

More:
https://arturbosch.github.io/detekt/extensions.html
https://proandroiddev.com/writing-custom-lint-rules-for-your-kotlin-project-with-detekt-653e4dbbe8b9
https://medium.com/@vanniktech/writing-your-first-detekt-rule-ee940e56428d

Gradle: Duplicated configuration in `android` block

Some elements within android block are duplicated in each module. Ideally this configuration should be applied only once (and work all the times, even for future modules).

android {
    compileSdkVersion(AndroidConfig.COMPILE_SDK_VERSION)

    defaultConfig {
        minSdkVersion(AndroidConfig.MIN_SDK_VERSION)
        targetSdkVersion(AndroidConfig.TARGET_SDK_VERSION)

        versionCode = AndroidConfig.VERSION_CODE
        versionName = AndroidConfig.VERSION_NAME
        testInstrumentationRunner = AndroidConfig.TEST_INSTRUMENTATION_RUNNER
    }
...

Fuel for network

Just an idea, to consider Fuel as an alternative to Retrofit using in particular the routing feature that allow you to easily have a nice API (quite reusable on iOS with Alamofire too)

Code coverage?

On top of quality checks with ktlint and detekt, it could be interesting to add coverage to your reports. I didn't find any mention of JaCoCo or any other option in your gradle files.

Data Flow in AlbumListFragment

Hi igor
As you explain in Data Flow section in image you show that in Album List Screen user pressed a button, then you delegate it to viewmodel

but again in the same screen you wrote:

albumAdapter.setOnDebouncedClickListener {
            
    val navDirections = AlbumListFragmentDirections
        .actionAlbumListToAlbumDetail(it.artist, it.name, it.mbId)
    findNavController().navigate(navDirections)
}

Shouldn't it be viewmodels's responsibility? why view decide to navigate to where instead of delegating it to viewmodel and the viewmodel decide to go where by notifying view?

Espresso Tests with Dynamic Feature Modules

Hi @igorwojda you're project is probably the most complete sample around!
Do you plan to add android UI tests?
I'm currently finding difficulties testing the UI with dynamic Feature Modules(with espresso) and was hoping to find something to reference.
Thanks,
Gabriele.

Unexpected end of stream

I/okhttp.OkHttpClient: --> POST http://ws.audioscrobbler.com/2.0/?method=album.search&album=sd&limit=60&api_key=70696db59158cb100370ad30a7a705c1&format=json
I/okhttp.OkHttpClient: Content-Length: 0
I/okhttp.OkHttpClient: User-Agent: showcase/1.0 Dalvik/2.1.0 (Linux; U; Android 10; Android SDK built for x86 Build/QSR1.200715.002)
I/okhttp.OkHttpClient: --> END POST (0-byte body)
I/okhttp.OkHttpClient: <-- HTTP FAILED: java.io.IOException: unexpected end of stream on http://ws.audioscrobbler.com/...

Actualy charles logs shows that response is OK
изображение

Db Support

Hi @igorwojda Thanks for great sample project.
Did you plan to add also support for Db layer ?
if not in the near future, can you please help design what will be the best approach to add db representation, in order to be accesses from all the modules.
Thanks,
Ronny.

Help creating APK

Hi @igorwojda ,
I'm trying to create debug APK using the AS option - Build bundle -> Build APK
The build succeeded, but when I'm trying to install the APK to device, it's crashes on some class not found errors...
Is it working for you, when you install the APK using the generated APK ?
Thanks,
Ronny.

App Crash

The first time when you run the app, it will display normally, and the second time it will crash. The error is as follows:

java.net.SocketTimeoutException: failed to connect to ws.audioscrobbler.com/64.30.224.206 (port 80) from /10.60.206.65 (port 40406) after 10000ms at libcore.io.IoBridge.connectErrno(IoBridge.java:185) at libcore.io.IoBridge.connect(IoBridge.java:130) at java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:129) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:356) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:356) at java.net.Socket.connect(Socket.java:616) at okhttp3.internal.platform.AndroidPlatform.connectSocket(AndroidPlatform.kt:56) at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.kt:268) at okhttp3.internal.connection.RealConnection.connect(RealConnection.kt:176) at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.kt:236) at okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.kt:109) at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.kt:77) at okhttp3.internal.connection.Transmitter.newExchange$okhttp(Transmitter.kt:162) at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:35) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87) at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:82) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87) at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:84) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112) at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:71) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87) at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.kt:215) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87) at com.igorwojda.showcase.app.data.retrofit.UserAgentInterceptor.intercept(UserAgentInterceptor.kt:23) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87) at com.igorwojda.showcase.app.data.retrofit.AuthenticationInterceptor.intercept(AuthenticationInterceptor.kt:18) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87) at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.kt:184) at okhttp3.RealCall$AsyncCall.run(RealCall.kt:136) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636) at java.lang.Thread.run(Thread.java:764)

Well! This seems to be a problem with the network.

Cannot run module

Hi, I have cloned your repo and wait for build. Everything was ok. But, I don't see any configuration to run. I have also try gradle sync, inv. cache and restart but nothing works.

Android studio version is: 3.5.3

Demo instructions missing

A description of a demo search would be useful. The app launches with "search album" tag, but it's not clear whether it's a music or photo album.

Having searched for adele, the list did not populate with any results, yet there appear items clickable, which launch a new screen with images missing. This is potentially a separate issue.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.