Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a>
from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
2 changes: 1 addition & 1 deletion TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion application/common/util-native/src/main/jni/Secure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#include <sys/stat.h>
#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<std::string> allowedPackages = { // NOLINT(cert-err58-cpp)
"tools.forma.sample.app"
};
Expand Down
2 changes: 1 addition & 1 deletion application/feature/characters/core/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion application/feature/characters/detail/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
2 changes: 1 addition & 1 deletion application/feature/characters/detail/res/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion application/feature/characters/list/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
api(
packageName = "tools.forma.sample.character.list.api",
packageName = "tools.forma.sample.feature.characters.list.api",
owner = Teams.core,
)
Empty file.
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions application/feature/characters/list/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
2 changes: 1 addition & 1 deletion application/feature/characters/list/res/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package tools.forma.sample.feature.characters.list.viewbinding.domain.model

interface ICharactersListViewEvent
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down
2 changes: 1 addition & 1 deletion application/feature/home/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
api(
packageName = "tools.forma.sample.home.api",
packageName = "tools.forma.sample.feature.home.api",
owner = Teams.core
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tools.forma.sample.feature.home.api

/**
* Public contracts for the home shell feature (bottom nav host).
*/
object FeatureHomeApi

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion application/root-res/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
androidRes(
packageName = "tools.forma.sample.rootres",
packageName = "tools.forma.sample.root.res",
dependencies = deps(
androidx.constraintlayout,
androidx.navigation
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<item
android:id="@+id/menu_toggle_theme"
android:title="@string/home_toolbar_theme"
app:actionViewClass="tools.forma.sample.feature.home.impl.ui.menu.ToggleThemeCheckBox"
app:actionViewClass="tools.forma.sample.widget.toggle.ToggleThemeCheckBox"
app:showAsAction="always" />

</menu>
13 changes: 8 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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
Expand All @@ -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`):

Expand Down
3 changes: 2 additions & 1 deletion docs/DEPENDENCY-MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
22 changes: 22 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Loading
Loading