diff --git a/README.md b/README.md index bde691ed..b0422933 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,8 @@ That document is generated from each target’s Dagger2-friendly `api`/`impl` + composition roots: **F-011** (landed). Jetpack Compose: per-target `compose` flag + `composeWidget` — **F-013** ([`docs/COMPOSE.md`](docs/COMPOSE.md)). +Sample multi-feature gold standard: **F-014** +([`docs/SAMPLE-APP.md`](docs/SAMPLE-APP.md)). Icons made by Freepik from www.flaticon.com diff --git a/TICKETS.md b/TICKETS.md index d56a165c..0a8075c1 100644 --- a/TICKETS.md +++ b/TICKETS.md @@ -21,7 +21,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi | F-011 | done | Tighten validation for `api` / `impl` (Dagger2-friendly boundaries) | app/binary/androidLibrary no longer EmptyValidator; impl still no→impl; docs matrix updated | | F-012 | done | External deps catalog UX + tooling polish | `library()` + pure generators + GAV validation + unit tests + `docs/DEPS-CATALOG.md` | | F-013 | done | Compose support for Android library/ui targets | GH #96; `compose` flags + `composeWidget`; `docs/COMPOSE.md` | -| F-014 | todo | Sample app: gold-standard multi-feature structure | Home/characters already present; modernize | +| F-014 | done | Sample app: gold-standard multi-feature structure | packageName + source-root alignment; `docs/SAMPLE-APP.md`; home/characters pattern | | F-015 | todo | Android project tutorial (getting started) | GH #53 | | F-016 | todo | Plugin publish path (Portal user + target publish config) | GH #132, #133 | | F-017 | todo | Configuration-time performance pass | GH #106, #42 | diff --git a/application/common/extensions/android-util/src/main/java/com/stepango/blockme/common/extensions/android/util/NavigationExtensions.kt b/application/common/extensions/android-util/src/main/java/tools/forma/sample/common/extensions/android/util/NavigationExtensions.kt similarity index 100% rename from application/common/extensions/android-util/src/main/java/com/stepango/blockme/common/extensions/android/util/NavigationExtensions.kt rename to application/common/extensions/android-util/src/main/java/tools/forma/sample/common/extensions/android/util/NavigationExtensions.kt diff --git a/application/common/extensions/util/src/main/java/com/stepango/blockme/common/extensions/util/ByteArrayExtensions.kt b/application/common/extensions/util/src/main/java/tools/forma/sample/common/extensions/util/ByteArrayExtensions.kt similarity index 100% rename from application/common/extensions/util/src/main/java/com/stepango/blockme/common/extensions/util/ByteArrayExtensions.kt rename to application/common/extensions/util/src/main/java/tools/forma/sample/common/extensions/util/ByteArrayExtensions.kt diff --git a/application/common/extensions/util/src/main/java/com/stepango/blockme/common/extensions/util/StringExtensions.kt b/application/common/extensions/util/src/main/java/tools/forma/sample/common/extensions/util/StringExtensions.kt similarity index 100% rename from application/common/extensions/util/src/main/java/com/stepango/blockme/common/extensions/util/StringExtensions.kt rename to application/common/extensions/util/src/main/java/tools/forma/sample/common/extensions/util/StringExtensions.kt diff --git a/application/common/util-native/src/main/jni/Secure.cpp b/application/common/util-native/src/main/jni/Secure.cpp index bccc4f5e..18170e8e 100644 --- a/application/common/util-native/src/main/jni/Secure.cpp +++ b/application/common/util-native/src/main/jni/Secure.cpp @@ -5,7 +5,7 @@ #include #include "Log.h" -static const char* kClassPathName = "com/stepango/blockme/root/library/AppStartup"; +static const char* kClassPathName = "tools/forma/sample/root/library/AppStartup"; static const std::vector allowedPackages = { // NOLINT(cert-err58-cpp) "tools.forma.sample.app" }; diff --git a/application/core/mvvm/library/src/main/java/com/stepango/blockme/core/mvvm/library/lifecycle/SingleLiveData.kt b/application/core/mvvm/library/src/main/java/tools/forma/sample/core/mvvm/library/lifecycle/SingleLiveData.kt similarity index 100% rename from application/core/mvvm/library/src/main/java/com/stepango/blockme/core/mvvm/library/lifecycle/SingleLiveData.kt rename to application/core/mvvm/library/src/main/java/tools/forma/sample/core/mvvm/library/lifecycle/SingleLiveData.kt diff --git a/application/core/mvvm/library/src/main/java/com/stepango/blockme/core/mvvm/library/lifecycle/ViewModelFactory.kt b/application/core/mvvm/library/src/main/java/tools/forma/sample/core/mvvm/library/lifecycle/ViewModelFactory.kt similarity index 100% rename from application/core/mvvm/library/src/main/java/com/stepango/blockme/core/mvvm/library/lifecycle/ViewModelFactory.kt rename to application/core/mvvm/library/src/main/java/tools/forma/sample/core/mvvm/library/lifecycle/ViewModelFactory.kt diff --git a/application/feature/characters/core/api/build.gradle.kts b/application/feature/characters/core/api/build.gradle.kts index e3d01d3f..904c19d1 100644 --- a/application/feature/characters/core/api/build.gradle.kts +++ b/application/feature/characters/core/api/build.gradle.kts @@ -1,5 +1,5 @@ api( - packageName = "tools.forma.sample.common.extensions.util", + packageName = "tools.forma.sample.feature.characters.core.api", owner = Teams.core, dependencies = deps( squareup.retrofit diff --git a/application/feature/characters/core/api/src/main/java/com/stepango/blockme/feature/characters/core/api/data/service/MarvelService.kt b/application/feature/characters/core/api/src/main/java/tools/forma/sample/feature/characters/core/api/data/service/MarvelService.kt similarity index 100% rename from application/feature/characters/core/api/src/main/java/com/stepango/blockme/feature/characters/core/api/data/service/MarvelService.kt rename to application/feature/characters/core/api/src/main/java/tools/forma/sample/feature/characters/core/api/data/service/MarvelService.kt diff --git a/application/feature/characters/core/impl/src/main/java/com/stepango/blockme/feature/characters/core/impl/data/mapper/CharacterMapper.kt b/application/feature/characters/core/impl/src/main/java/tools/forma/sample/feature/characters/core/impl/data/mapper/CharacterMapper.kt similarity index 100% rename from application/feature/characters/core/impl/src/main/java/com/stepango/blockme/feature/characters/core/impl/data/mapper/CharacterMapper.kt rename to application/feature/characters/core/impl/src/main/java/tools/forma/sample/feature/characters/core/impl/data/mapper/CharacterMapper.kt diff --git a/application/feature/characters/core/impl/src/main/java/com/stepango/blockme/feature/characters/core/impl/domain/repository/ServiceMarvelRepository.kt b/application/feature/characters/core/impl/src/main/java/tools/forma/sample/feature/characters/core/impl/domain/repository/ServiceMarvelRepository.kt similarity index 100% rename from application/feature/characters/core/impl/src/main/java/com/stepango/blockme/feature/characters/core/impl/domain/repository/ServiceMarvelRepository.kt rename to application/feature/characters/core/impl/src/main/java/tools/forma/sample/feature/characters/core/impl/domain/repository/ServiceMarvelRepository.kt diff --git a/application/feature/characters/core/impl/src/test/java/com/stepango/blockme/feature/characters/core/impl/domain/repository/ServiceMarvelRepositoryTest.kt b/application/feature/characters/core/impl/src/test/java/tools/forma/sample/feature/characters/core/impl/domain/repository/ServiceMarvelRepositoryTest.kt similarity index 100% rename from application/feature/characters/core/impl/src/test/java/com/stepango/blockme/feature/characters/core/impl/domain/repository/ServiceMarvelRepositoryTest.kt rename to application/feature/characters/core/impl/src/test/java/tools/forma/sample/feature/characters/core/impl/domain/repository/ServiceMarvelRepositoryTest.kt diff --git a/application/feature/characters/detail/api/build.gradle.kts b/application/feature/characters/detail/api/build.gradle.kts index e8383bfa..10f9694b 100644 --- a/application/feature/characters/detail/api/build.gradle.kts +++ b/application/feature/characters/detail/api/build.gradle.kts @@ -1,5 +1,5 @@ api( - packageName = "tools.forma.sample.character.detail.api", + packageName = "tools.forma.sample.feature.characters.detail.api", owner = Teams.core, dependencies = deps( target(":feature:characters:core:api") diff --git a/application/feature/characters/detail/impl/src/main/java/tools/forma/sample/feature/characters/detail/impl/ui/CharacterDetailFragment.kt b/application/feature/characters/detail/impl/src/main/java/tools/forma/sample/feature/characters/detail/impl/ui/CharacterDetailFragment.kt index b5c63204..b0a6abf7 100644 --- a/application/feature/characters/detail/impl/src/main/java/tools/forma/sample/feature/characters/detail/impl/ui/CharacterDetailFragment.kt +++ b/application/feature/characters/detail/impl/src/main/java/tools/forma/sample/feature/characters/detail/impl/ui/CharacterDetailFragment.kt @@ -91,13 +91,13 @@ class CharacterDetailFragment : BaseViewBindingFragment( viewBinding.addFavoriteButton.isVisible = viewState.isAddToFavorite() when (viewState) { is CharacterDetailViewState.Loading -> - progressDialog.show(tools.forma.sample.feature.character.detail.res.R.string.character_detail_dialog_loading_text) + progressDialog.show(tools.forma.sample.feature.characters.detail.res.R.string.character_detail_dialog_loading_text) is CharacterDetailViewState.Error -> - progressDialog.dismissWithErrorMessage(tools.forma.sample.feature.character.detail.res.R.string.character_detail_dialog_error_text) + progressDialog.dismissWithErrorMessage(tools.forma.sample.feature.characters.detail.res.R.string.character_detail_dialog_error_text) is CharacterDetailViewState.AddedToFavorite -> Snackbar.make( requireView(), - tools.forma.sample.feature.character.detail.res.R.string.character_detail_added_to_favorite_message, + tools.forma.sample.feature.characters.detail.res.R.string.character_detail_added_to_favorite_message, Snackbar.LENGTH_LONG ).show() is CharacterDetailViewState.Dismiss -> diff --git a/application/feature/characters/detail/res/build.gradle.kts b/application/feature/characters/detail/res/build.gradle.kts index 45963af0..9191acc6 100644 --- a/application/feature/characters/detail/res/build.gradle.kts +++ b/application/feature/characters/detail/res/build.gradle.kts @@ -1,5 +1,5 @@ androidRes( - packageName = "tools.forma.sample.feature.character.detail.res", + packageName = "tools.forma.sample.feature.characters.detail.res", dependencies = deps( google.material, androidx.appcompat, diff --git a/application/feature/characters/favorite/api/build.gradle.kts b/application/feature/characters/favorite/api/build.gradle.kts index a037e134..0d1f1fb8 100644 --- a/application/feature/characters/favorite/api/build.gradle.kts +++ b/application/feature/characters/favorite/api/build.gradle.kts @@ -1,5 +1,5 @@ api( - packageName = "tools.forma.sample.character.favorite.api", + packageName = "tools.forma.sample.feature.characters.favorite.api", owner = Teams.core, dependencies = deps( target(":feature:characters:core:api") diff --git a/application/feature/characters/favorite/impl/src/main/java/tools/forma/sample/feature/characters/favorite/impl/ui/CharacterFavoriteFragment.kt b/application/feature/characters/favorite/impl/src/main/java/tools/forma/sample/feature/characters/favorite/impl/ui/CharacterFavoriteFragment.kt index 504436f9..48912349 100644 --- a/application/feature/characters/favorite/impl/src/main/java/tools/forma/sample/feature/characters/favorite/impl/ui/CharacterFavoriteFragment.kt +++ b/application/feature/characters/favorite/impl/src/main/java/tools/forma/sample/feature/characters/favorite/impl/ui/CharacterFavoriteFragment.kt @@ -33,10 +33,10 @@ import tools.forma.sample.feature.characters.favorite.impl.R import tools.forma.sample.feature.characters.favorite.impl.di.DaggerCharacterFavoriteComponent import tools.forma.sample.feature.characters.favorite.impl.ui.adapter.CharacterFavoriteAdapter import tools.forma.sample.feature.characters.favorite.impl.ui.adapter.CharacterFavoriteTouchHelper -import tools.forma.sample.feature.favorite.viewbinding.databinding.FragmentCharacterFavoriteListBinding +import tools.forma.sample.feature.characters.favorite.viewbinding.databinding.FragmentCharacterFavoriteListBinding class CharacterFavoriteFragment : BaseViewBindingFragment( - layoutId = tools.forma.sample.feature.favorite.viewbinding.R.layout.fragment_character_favorite_list + layoutId = tools.forma.sample.feature.characters.favorite.viewbinding.R.layout.fragment_character_favorite_list ) { private val viewModel: ICharacterFavoriteViewModel by viewModels() @@ -64,7 +64,7 @@ class CharacterFavoriteFragment : BaseViewBindingFragment( viewBinding.includeList.charactersFavoriteList.apply { addItemDecoration( RecyclerViewItemDecoration( - resources.getDimensionPixelSize(tools.forma.sample.feature.character.favorite.res.R.dimen.characters_favorite_list_item_padding) + resources.getDimensionPixelSize(tools.forma.sample.feature.characters.favorite.res.R.dimen.characters_favorite_list_item_padding) ) ) adapter = viewAdapter diff --git a/application/feature/characters/favorite/impl/src/main/java/tools/forma/sample/feature/characters/favorite/impl/ui/adapter/holders/CharacterFavoriteViewHolder.kt b/application/feature/characters/favorite/impl/src/main/java/tools/forma/sample/feature/characters/favorite/impl/ui/adapter/holders/CharacterFavoriteViewHolder.kt index 31c468b0..2b3921db 100644 --- a/application/feature/characters/favorite/impl/src/main/java/tools/forma/sample/feature/characters/favorite/impl/ui/adapter/holders/CharacterFavoriteViewHolder.kt +++ b/application/feature/characters/favorite/impl/src/main/java/tools/forma/sample/feature/characters/favorite/impl/ui/adapter/holders/CharacterFavoriteViewHolder.kt @@ -20,7 +20,7 @@ import android.view.LayoutInflater import tools.forma.sample.common.extensions.android.util.loadImage import tools.forma.sample.core.mvvm.library.ui.BaseViewHolder import tools.forma.sample.feature.characters.core.api.domain.model.ICharacter -import tools.forma.sample.feature.favorite.viewbinding.databinding.ListItemCharacterFavoriteBinding +import tools.forma.sample.feature.characters.favorite.viewbinding.databinding.ListItemCharacterFavoriteBinding class CharacterFavoriteViewHolder( inflater: LayoutInflater diff --git a/application/feature/characters/favorite/res/build.gradle.kts b/application/feature/characters/favorite/res/build.gradle.kts index 17d0262c..98fe64fe 100644 --- a/application/feature/characters/favorite/res/build.gradle.kts +++ b/application/feature/characters/favorite/res/build.gradle.kts @@ -1,5 +1,5 @@ androidRes( - packageName = "tools.forma.sample.feature.character.favorite.res", + packageName = "tools.forma.sample.feature.characters.favorite.res", dependencies = deps( google.material, androidx.appcompat, diff --git a/application/feature/characters/favorite/viewbinding/build.gradle.kts b/application/feature/characters/favorite/viewbinding/build.gradle.kts index af2b3acc..d8657f6a 100644 --- a/application/feature/characters/favorite/viewbinding/build.gradle.kts +++ b/application/feature/characters/favorite/viewbinding/build.gradle.kts @@ -1,5 +1,5 @@ viewBinding( - packageName = "tools.forma.sample.feature.favorite.viewbinding", + packageName = "tools.forma.sample.feature.characters.favorite.viewbinding", dependencies = deps( google.material, androidx.navigation, diff --git a/application/feature/characters/list/api/build.gradle.kts b/application/feature/characters/list/api/build.gradle.kts index f9e99b3b..3ab959ff 100644 --- a/application/feature/characters/list/api/build.gradle.kts +++ b/application/feature/characters/list/api/build.gradle.kts @@ -1,4 +1,4 @@ api( - packageName = "tools.forma.sample.character.list.api", + packageName = "tools.forma.sample.feature.characters.list.api", owner = Teams.core, ) \ No newline at end of file diff --git a/application/feature/characters/list/api/src/main/empty.kt b/application/feature/characters/list/api/src/main/empty.kt deleted file mode 100644 index e69de29b..00000000 diff --git a/application/feature/characters/list/api/src/main/java/tools/forma/sample/feature/characters/list/api/FeatureCharactersListApi.kt b/application/feature/characters/list/api/src/main/java/tools/forma/sample/feature/characters/list/api/FeatureCharactersListApi.kt new file mode 100644 index 00000000..790cb80c --- /dev/null +++ b/application/feature/characters/list/api/src/main/java/tools/forma/sample/feature/characters/list/api/FeatureCharactersListApi.kt @@ -0,0 +1,7 @@ +package tools.forma.sample.feature.characters.list.api + +/** + * Public contracts for the characters-list feature. + * UI wiring lives in `impl` / `viewbinding`; keep this module dependency-light. + */ +object FeatureCharactersListApi diff --git a/application/feature/characters/list/impl/build.gradle.kts b/application/feature/characters/list/impl/build.gradle.kts index 263fc093..5fa18173 100644 --- a/application/feature/characters/list/impl/build.gradle.kts +++ b/application/feature/characters/list/impl/build.gradle.kts @@ -16,9 +16,9 @@ impl( viewbinding.viewpropertydelegate, ) + deps(libs.jakewhartonTimber) + - deps(projects.featureCharactersCoreApi) + deps( - target(projects.featureCharactersListApi), + target(":feature:characters:core:api"), + target(":feature:characters:list:api"), target(":feature:characters:list:viewbinding"), target(":feature:characters:list:res"), target(":core:di:library"), diff --git a/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListFragment.kt b/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListFragment.kt index 7d94519e..aafaafb8 100644 --- a/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListFragment.kt +++ b/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListFragment.kt @@ -32,8 +32,8 @@ import tools.forma.sample.feature.characters.core.api.domain.model.ICharacter import tools.forma.sample.feature.characters.list.viewbinding.databinding.FragmentCharactersListBinding import tools.forma.sample.feature.characters.list.impl.R import tools.forma.sample.feature.characters.list.impl.di.DaggerCharactersListComponent -import tools.forma.sample.feature.characters.list.impl.domain.model.ICharactersListViewEvent -import tools.forma.sample.feature.characters.list.impl.domain.model.ICharactersListViewState +import tools.forma.sample.feature.characters.list.viewbinding.domain.model.ICharactersListViewEvent +import tools.forma.sample.feature.characters.list.viewbinding.domain.model.ICharactersListViewState import tools.forma.sample.feature.characters.list.impl.ui.adapter.CharactersListAdapter import tools.forma.sample.feature.characters.list.impl.ui.adapter.CharactersListAdapterState @@ -70,7 +70,7 @@ class CharactersListFragment : BaseViewBindingFragment( viewBinding.includeList.charactersList.apply { addItemDecoration( RecyclerViewItemDecoration( - resources.getDimensionPixelSize(tools.forma.sample.feature.character.list.res.R.dimen.characters_list_item_padding) + resources.getDimensionPixelSize(tools.forma.sample.feature.characters.list.res.R.dimen.characters_list_item_padding) ) ) adapter = viewAdapter diff --git a/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewEvent.kt b/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewEvent.kt index ec366dad..cc02e5a5 100644 --- a/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewEvent.kt +++ b/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewEvent.kt @@ -16,7 +16,7 @@ package tools.forma.sample.feature.characters.list.impl.ui -import tools.forma.sample.feature.characters.list.impl.domain.model.ICharactersListViewEvent +import tools.forma.sample.feature.characters.list.viewbinding.domain.model.ICharactersListViewEvent sealed class CharactersListViewEvent : ICharactersListViewEvent { diff --git a/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewModel.kt b/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewModel.kt index 43e8ebb9..949bd9c7 100644 --- a/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewModel.kt +++ b/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewModel.kt @@ -26,9 +26,9 @@ import tools.forma.sample.core.network.library.NetworkState import tools.forma.sample.feature.characters.list.impl.data.datasource.CharactersPageDataSourceFactory import tools.forma.sample.feature.characters.list.impl.data.datasource.PAGE_MAX_ELEMENTS import tools.forma.sample.feature.characters.core.api.domain.model.ICharacter -import tools.forma.sample.feature.characters.list.impl.domain.model.ICharactersListViewEvent -import tools.forma.sample.feature.characters.list.impl.domain.model.ICharactersListViewModel -import tools.forma.sample.feature.characters.list.impl.domain.model.ICharactersListViewState +import tools.forma.sample.feature.characters.list.viewbinding.domain.model.ICharactersListViewEvent +import tools.forma.sample.feature.characters.list.viewbinding.domain.model.ICharactersListViewModel +import tools.forma.sample.feature.characters.list.viewbinding.domain.model.ICharactersListViewState import javax.inject.Inject class CharactersListViewModel @Inject constructor( diff --git a/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewState.kt b/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewState.kt index b2b2bc04..d1b6c2b0 100644 --- a/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewState.kt +++ b/application/feature/characters/list/impl/src/main/java/tools/forma/sample/feature/characters/list/impl/ui/CharactersListViewState.kt @@ -17,7 +17,7 @@ package tools.forma.sample.feature.characters.list.impl.ui import tools.forma.sample.core.mvvm.library.ui.BaseViewState -import tools.forma.sample.feature.characters.list.impl.domain.model.ICharactersListViewState +import tools.forma.sample.feature.characters.list.viewbinding.domain.model.ICharactersListViewState sealed class CharactersListViewState : BaseViewState, ICharactersListViewState { diff --git a/application/feature/characters/list/res/build.gradle.kts b/application/feature/characters/list/res/build.gradle.kts index 5cf7c39e..5285fd61 100644 --- a/application/feature/characters/list/res/build.gradle.kts +++ b/application/feature/characters/list/res/build.gradle.kts @@ -1,5 +1,5 @@ androidRes( - packageName = "tools.forma.sample.feature.character.list.res", + packageName = "tools.forma.sample.feature.characters.list.res", dependencies = deps( google.material, androidx.appcompat, diff --git a/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/impl/domain/model/ICharactersListViewEvent.kt b/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/impl/domain/model/ICharactersListViewEvent.kt deleted file mode 100644 index be9a2ee0..00000000 --- a/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/impl/domain/model/ICharactersListViewEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package tools.forma.sample.feature.characters.list.impl.domain.model - -interface ICharactersListViewEvent \ No newline at end of file diff --git a/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/viewbinding/domain/model/ICharactersListViewEvent.kt b/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/viewbinding/domain/model/ICharactersListViewEvent.kt new file mode 100644 index 00000000..8257658c --- /dev/null +++ b/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/viewbinding/domain/model/ICharactersListViewEvent.kt @@ -0,0 +1,3 @@ +package tools.forma.sample.feature.characters.list.viewbinding.domain.model + +interface ICharactersListViewEvent \ No newline at end of file diff --git a/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/impl/domain/model/ICharactersListViewModel.kt b/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/viewbinding/domain/model/ICharactersListViewModel.kt similarity index 92% rename from application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/impl/domain/model/ICharactersListViewModel.kt rename to application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/viewbinding/domain/model/ICharactersListViewModel.kt index 59225d7c..7a6aabf7 100644 --- a/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/impl/domain/model/ICharactersListViewModel.kt +++ b/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/viewbinding/domain/model/ICharactersListViewModel.kt @@ -1,4 +1,4 @@ -package tools.forma.sample.feature.characters.list.impl.domain.model +package tools.forma.sample.feature.characters.list.viewbinding.domain.model import androidx.lifecycle.LiveData import androidx.paging.PagedList diff --git a/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/impl/domain/model/ICharactersListViewState.kt b/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/viewbinding/domain/model/ICharactersListViewState.kt similarity index 94% rename from application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/impl/domain/model/ICharactersListViewState.kt rename to application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/viewbinding/domain/model/ICharactersListViewState.kt index 08830817..675e3679 100644 --- a/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/impl/domain/model/ICharactersListViewState.kt +++ b/application/feature/characters/list/viewbinding/src/main/java/tools/forma/sample/feature/characters/list/viewbinding/domain/model/ICharactersListViewState.kt @@ -1,4 +1,4 @@ -package tools.forma.sample.feature.characters.list.impl.domain.model +package tools.forma.sample.feature.characters.list.viewbinding.domain.model interface ICharactersListViewState { /** diff --git a/application/feature/home/api/build.gradle.kts b/application/feature/home/api/build.gradle.kts index 2277c543..34f08309 100644 --- a/application/feature/home/api/build.gradle.kts +++ b/application/feature/home/api/build.gradle.kts @@ -1,4 +1,4 @@ api( - packageName = "tools.forma.sample.home.api", + packageName = "tools.forma.sample.feature.home.api", owner = Teams.core ) \ No newline at end of file diff --git a/application/feature/home/api/src/main/java/tools/forma/sample/feature/home/api/FeatureHomeApi.kt b/application/feature/home/api/src/main/java/tools/forma/sample/feature/home/api/FeatureHomeApi.kt new file mode 100644 index 00000000..962590ff --- /dev/null +++ b/application/feature/home/api/src/main/java/tools/forma/sample/feature/home/api/FeatureHomeApi.kt @@ -0,0 +1,6 @@ +package tools.forma.sample.feature.home.api + +/** + * Public contracts for the home shell feature (bottom nav host). + */ +object FeatureHomeApi diff --git a/application/feature/home/api/src/main/java/tools/forma/sample/feature/home/api/empty.kt b/application/feature/home/api/src/main/java/tools/forma/sample/feature/home/api/empty.kt deleted file mode 100644 index 1200f2ce..00000000 --- a/application/feature/home/api/src/main/java/tools/forma/sample/feature/home/api/empty.kt +++ /dev/null @@ -1,2 +0,0 @@ -package tools.forma.sample.feature.home.api - diff --git a/application/feature/home/impl/src/main/java/tools/forma/sample/feature/home/impl/di/HomeModule.kt b/application/feature/home/impl/src/main/java/tools/forma/sample/feature/home/impl/di/HomeModule.kt index 980700cd..f02582ae 100644 --- a/application/feature/home/impl/src/main/java/tools/forma/sample/feature/home/impl/di/HomeModule.kt +++ b/application/feature/home/impl/src/main/java/tools/forma/sample/feature/home/impl/di/HomeModule.kt @@ -3,7 +3,7 @@ package tools.forma.sample.feature.home.impl.di import androidx.lifecycle.ViewModel import tools.forma.sample.core.di.library.scopes.FeatureScope import tools.forma.sample.core.mvvm.library.di.ViewModelKey -import tools.forma.sample.feature.home.impl.ui.HomeViewModel +import tools.forma.sample.feature.home.viewbinding.ui.HomeViewModel import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap diff --git a/application/feature/home/impl/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeFragment.kt b/application/feature/home/impl/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeFragment.kt index 665a4a9e..90546696 100644 --- a/application/feature/home/impl/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeFragment.kt +++ b/application/feature/home/impl/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeFragment.kt @@ -32,8 +32,9 @@ import tools.forma.sample.core.theme.android.util.ThemeUtils import tools.forma.sample.core.theme.android.util.di.ThemeComponentProvider import tools.forma.sample.feature.home.impl.R import tools.forma.sample.feature.home.impl.di.DaggerHomeComponent -import tools.forma.sample.feature.home.impl.ui.menu.ToggleThemeCheckBox import tools.forma.sample.feature.home.viewbinding.databinding.FragmentHomeBinding +import tools.forma.sample.feature.home.viewbinding.ui.HomeViewModel +import tools.forma.sample.widget.toggle.ToggleThemeCheckBox import javax.inject.Inject private const val DELAY_TO_APPLY_THEME = 1000L diff --git a/application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeViewModel.kt b/application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/viewbinding/ui/HomeViewModel.kt similarity index 96% rename from application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeViewModel.kt rename to application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/viewbinding/ui/HomeViewModel.kt index 6951a5dc..287469ad 100644 --- a/application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeViewModel.kt +++ b/application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/viewbinding/ui/HomeViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package tools.forma.sample.feature.home.impl.ui +package tools.forma.sample.feature.home.viewbinding.ui import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData diff --git a/application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeViewState.kt b/application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/viewbinding/ui/HomeViewState.kt similarity index 94% rename from application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeViewState.kt rename to application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/viewbinding/ui/HomeViewState.kt index f0fcc54e..7ffa35cc 100644 --- a/application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/impl/ui/HomeViewState.kt +++ b/application/feature/home/viewbinding/src/main/java/tools/forma/sample/feature/home/viewbinding/ui/HomeViewState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package tools.forma.sample.feature.home.impl.ui +package tools.forma.sample.feature.home.viewbinding.ui import tools.forma.sample.core.mvvm.library.ui.BaseViewState diff --git a/application/root-app/src/main/java/tools/forma/sample/root/library/SampleMainActivity.kt b/application/root-app/src/main/java/tools/forma/sample/root/library/SampleMainActivity.kt index d898ac1a..371b8631 100644 --- a/application/root-app/src/main/java/tools/forma/sample/root/library/SampleMainActivity.kt +++ b/application/root-app/src/main/java/tools/forma/sample/root/library/SampleMainActivity.kt @@ -37,6 +37,6 @@ class SampleMainActivity : AppCompatActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(tools.forma.sample.rootres.R.layout.activity_main) + setContentView(tools.forma.sample.root.res.R.layout.activity_main) } } diff --git a/application/root-res/build.gradle.kts b/application/root-res/build.gradle.kts index 62477070..cccf702b 100644 --- a/application/root-res/build.gradle.kts +++ b/application/root-res/build.gradle.kts @@ -1,5 +1,5 @@ androidRes( - packageName = "tools.forma.sample.rootres", + packageName = "tools.forma.sample.root.res", dependencies = deps( androidx.constraintlayout, androidx.navigation diff --git a/application/toggle-widget/src/main/java/tools/forma/sample/feature/home/impl/ui/menu/ToggleThemeCheckBox.kt b/application/toggle-widget/src/main/java/tools/forma/sample/widget/toggle/ToggleThemeCheckBox.kt similarity index 89% rename from application/toggle-widget/src/main/java/tools/forma/sample/feature/home/impl/ui/menu/ToggleThemeCheckBox.kt rename to application/toggle-widget/src/main/java/tools/forma/sample/widget/toggle/ToggleThemeCheckBox.kt index 6db00978..9e7fe377 100644 --- a/application/toggle-widget/src/main/java/tools/forma/sample/feature/home/impl/ui/menu/ToggleThemeCheckBox.kt +++ b/application/toggle-widget/src/main/java/tools/forma/sample/widget/toggle/ToggleThemeCheckBox.kt @@ -1,4 +1,4 @@ -package tools.forma.sample.feature.home.impl.ui.menu +package tools.forma.sample.widget.toggle import android.content.Context import android.util.AttributeSet diff --git a/application/toggle-widget/src/main/res/menu/toolbar_menu.xml b/application/toggle-widget/src/main/res/menu/toolbar_menu.xml index fffbce5b..d18331c8 100644 --- a/application/toggle-widget/src/main/res/menu/toolbar_menu.xml +++ b/application/toggle-widget/src/main/res/menu/toolbar_menu.xml @@ -10,7 +10,7 @@ diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9d1222d4..5bd39e10 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -197,7 +197,8 @@ Sample enables `arbitraryBuildScriptNames = true`. ## 3. Sample `application/` structure -~35 Forma targets, multi-feature Rick-and-Morty style demo. +~35 Forma targets, multi-feature Marvel demo. **User-facing gold-standard +guide:** [`SAMPLE-APP.md`](SAMPLE-APP.md) (F-014). ``` application/ @@ -206,14 +207,15 @@ application/ ├── root-res/ androidRes ├── toggle-widget/ widget ├── core/ -│ ├── di/library library -│ ├── mvvm/library library -│ ├── navigation/library library -│ ├── network/library library +│ ├── di/library androidLibrary +│ ├── mvvm/library androidLibrary +│ ├── navigation/library androidLibrary +│ ├── network/library library (JVM) │ └── theme/{android-util,res} ├── common/ │ ├── util util │ ├── extensions/{util,android-util} +│ ├── greeting/compose-widget composeWidget │ ├── placeholder/res │ ├── progressbar/{res,viewbinding} │ └── recyclerview/widget @@ -232,6 +234,7 @@ Wiring pattern: - Feature **impl** = Android library + Dagger + navigation + viewbinding/res/widget - **binary** depends on root-app + all feature api/impl + shared core (explicit graph; not only transitive) +- `packageName` aligned to path under `tools.forma.sample…` (F-014) Root configuration (`application/build.gradle.kts`): diff --git a/docs/DEPENDENCY-MATRIX.md b/docs/DEPENDENCY-MATRIX.md index 5c3dc7d9..962b8036 100644 --- a/docs/DEPENDENCY-MATRIX.md +++ b/docs/DEPENDENCY-MATRIX.md @@ -209,7 +209,8 @@ Tightening unrestricted entry targets landed in **F-011**. - `binary` → `androidBinary` with explicit deps on root-app + feature api/impl (legal under composition-root allowlist; F-011) -See `docs/ARCHITECTURE.md` §2.2 and §3 for the module map. +**Gold-standard guide:** [`SAMPLE-APP.md`](SAMPLE-APP.md) (F-014). See also +`docs/ARCHITECTURE.md` §2.2 and §3 for the module map. --- diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 520bd987..c7891d5e 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,6 +2,28 @@ Newest entries first. +## 2026-07-12 — F-014 Sample app gold-standard multi-feature structure + +- **Ticket:** F-014 → `done` +- **Branch:** `forma/F-014-sample-gold-standard` (from `origin/v2`) +- **Sample modernization:** + - Relocated leftover `com/stepango/blockme/…` source trees under + `tools/forma/sample/…` (mvvm lifecycle, extensions, Marvel service/repo/mapper/tests) + - Aligned `packageName` with module path for home/characters api+res+viewbinding, + characters core api, root-res (`tools.forma.sample.root.res`) + - Moved `toggle-widget` sources into `tools.forma.sample.widget.toggle` (+ menu XML) + - Home viewbinding UI types under `…home.viewbinding.ui`; list contracts under + `…list.viewbinding.domain.model` (impl imports updated) + - Normalized `feature/characters/list/impl` deps to consistent `target(":…")` form + - Placeholder `FeatureHomeApi` / `FeatureCharactersListApi` objects for empty api modules +- **Docs:** `docs/SAMPLE-APP.md` gold-standard guide; ARCHITECTURE §3, DEPENDENCY-MATRIX, + README pointers; TICKETS status +- **Verify (real tool output, OpenJDK 17 + SDK 34):** + - `application/`: `./gradlew build` → **BUILD SUCCESSFUL** (2112 tasks, 54s after clean) +- **Commits/PRs:** this run — push + PR base `v2` +- **Blockers:** none +- **Next step:** F-015 Android project tutorial (getting started) + ## 2026-07-11 — F-013 Compose support (GH #96) - **Ticket:** F-013 → `done` diff --git a/docs/SAMPLE-APP.md b/docs/SAMPLE-APP.md new file mode 100644 index 00000000..306ada07 --- /dev/null +++ b/docs/SAMPLE-APP.md @@ -0,0 +1,122 @@ +# Sample Android app — gold-standard multi-feature structure (F-014) + +The `application/` composite build is Forma’s **product reference**: a multi-feature +Android app wired with the full target set (`api` / `impl` / `res` / `viewbinding` / +`widget` / `composeWidget` / composition roots). + +Use this layout when teaching Forma (F-015) or copying a starter structure. + +## Layout + +``` +application/ +├── binary/ androidBinary (APK / composition root) +├── root-app/ androidApp (Application + root DI) +├── root-res/ androidRes (launcher, app nav host layout) +├── toggle-widget/ widget (theme toggle action view) +├── core/ +│ ├── di/library androidLibrary (Dagger scopes / base component) +│ ├── mvvm/library androidLibrary (ViewModel helpers, adapters) +│ ├── navigation/library androidLibrary (feature nav graphs + safe-args) +│ ├── network/library library (JVM) (Retrofit / Config / NetworkState) +│ └── theme/{android-util,res} +├── common/ +│ ├── util util +│ ├── extensions/{util,android-util} +│ ├── greeting/compose-widget composeWidget (Compose sample — F-013) +│ ├── placeholder/res +│ ├── progressbar/{res,viewbinding} +│ └── recyclerview/widget +└── feature/ + ├── home/{api,impl,res,viewbinding} + └── characters/ + ├── core/{api,impl} (shared domain + Marvel API) + ├── list/{api,impl,res,viewbinding} + ├── detail/{api,impl,res,viewbinding} + └── favorite/{api,impl,res,viewbinding} +``` + +Includer discovers every `build.gradle.kts` under `application/` +(`arbitraryBuildScriptNames = true`). Project path → name uses `-` instead of `/` +(e.g. `feature/characters/list/impl` → `:feature-characters-list-impl` for Gradle +type-safe accessors; Forma `target(":feature:characters:list:impl")` uses colon +path form). + +## Feature slice convention + +| Slice | Target | Responsibility | +|-------|--------|----------------| +| `api` | `api` | JVM contracts, DI feature interfaces, domain models visible to other features | +| `impl` | `impl` | Android feature implementation (UI, ViewModels, Dagger feature components) | +| `res` | `androidRes` | Feature strings / dimens / drawables only | +| `viewbinding` | `viewBinding` | Layouts + light binding-facing types (interfaces for list/favorite UI contracts) | + +Rules enforced by validators ([DEPENDENCY-MATRIX.md](DEPENDENCY-MATRIX.md)): + +- **`impl` cannot depend on another `impl`** (Dagger-friendly). Cross-feature code + goes through `api` (or shared `core` libraries). +- **Composition roots** (`androidApp` / `androidBinary`) pull feature `api` + `impl` + explicitly — do not rely on transitive feature wiring alone. +- **`api` stays free of `res/`** under `src/main`. + +## Package naming + +Namespace / `packageName` follows the module path under `tools.forma.sample…`: + +| Module path | `packageName` | +|-------------|-----------------| +| `feature/home/api` | `tools.forma.sample.feature.home.api` | +| `feature/characters/list/impl` | `tools.forma.sample.feature.characters.list.impl` | +| `feature/characters/list/res` | `tools.forma.sample.feature.characters.list.res` | +| `feature/characters/favorite/viewbinding` | `tools.forma.sample.feature.characters.favorite.viewbinding` | +| `root-res` | `tools.forma.sample.root.res` | +| `toggle-widget` | `tools.forma.sample.widget.toggle` | +| `binary` | `tools.forma.sample.app` | + +Kotlin sources live under `src/main/java//…` matching the +declared package (no leftover third-party roots). + +Shared UI contracts that belong with layouts may live in the `viewbinding` +module package (e.g. list view-state interfaces under +`…list.viewbinding.domain.model`) so `impl` depends on them without putting +layouts into `impl`. + +## Wiring sketch + +``` +binary (androidBinary) + └── root-app (androidApp) ── root-res + └── feature/*/api + feature/*/impl (explicit) + └── common/greeting/compose-widget (Compose demo) + └── core/* shared libraries + +root-app hosts SampleApp DI: + BaseComponent + ThemeComponent + CharactersCore + CharacterFavorite + +home/impl hosts bottom navigation: + navigation_characters_list_graph + navigation_character_favorite_graph + (from core/navigation/library) +``` + +## Build + +```bash +source scripts/env-mac.sh +printf 'sdk.dir=%s\n' "$ANDROID_HOME" > application/local.properties +cd application && ./gradlew build +``` + +Toolchain: AGP **8.1.2**, compileSdk **34**, targetSdk **33**, Gradle **8.4**, +JDK **17**. See [ENV.md](ENV.md), [COMPOSE.md](COMPOSE.md), [ARCHITECTURE.md](ARCHITECTURE.md). + +## What “gold standard” means here + +1. **One feature = one folder** with consistent `api` / `impl` / `res` / `viewbinding` slices. +2. **Aligned packageNames** with directory + Kotlin packages. +3. **Explicit composition-root deps** on every feature entrypoint. +4. **Shared platform code** under `core/` and `common/`, not copied per feature. +5. **Modern targets** represented: View widgets + Compose widget side by side. +6. **Documented** enough to copy without reading every `build.gradle.kts`. + +Not in scope for F-014: full navigation redesign (GH #46), publish path (F-016), +or configuration-time performance (F-017).