Copying a feature into a second Flutter app feels fast. Six months later, one app has the bug fix, the other has the new API contract, and nobody is sure which implementation is correct.
Moving everything into a shared package creates the opposite problem. The package starts owning routes, themes, app initialization, and product-specific behavior. Every change for one app risks the other.
The useful boundary is not “shared code.” It is shared capability with app-owned composition.
Share complete feature logic#
When two apps use the same feature, keep its reusable pipeline together:
API
↓
Service + response model
↓
Repository + domain entity
↓
Use case
↓
BLoC
↓
Reusable page or widgets
If only a model is shared while each app rewrites the repository and BLoC, behavior will still drift.
A shared package can expose a focused entry point:
export 'src/favourites/entities/favourite.dart';
export 'src/favourites/bloc/favourites_bloc.dart';
export 'src/favourites/widgets/favourite_button.dart';
Keep internal services and mapping helpers private unless applications genuinely need them.
Keep the application shell outside#
The app should own:
- the root route tree;
- authentication and onboarding redirects;
- navigation bars and tabs;
- app bootstrap;
- dependency registrations;
- branding and app-only integrations.
A shared widget should request navigation as a callback:
class FavouriteCard extends StatelessWidget {
const FavouriteCard({
required this.item,
required this.onOpen,
super.key,
});
final Favourite item;
final ValueChanged<Favourite> onOpen;
}
Each app decides whether opening that item means a named route, a shell branch, or a modal player.
Inject differences instead of checking the app#
Avoid conditions throughout the package:
if (currentApp == App.brandA) {
// ...
} else {
// ...
}
Represent a real variation as a contract:
abstract interface class FeaturePresentation {
String emptyTitle(BuildContext context);
Widget buildHeader(BuildContext context);
}
Each app supplies its implementation. Business behavior stays shared, while brand presentation remains app-owned.
Use this sparingly. If every widget needs a strategy interface, the feature may not be as shared as expected.
Let apps assemble dependencies#
The package can define constructors and interfaces:
class FavouritesBloc extends Bloc<FavouritesEvent, FavouritesState> {
FavouritesBloc(
this._loadFavourites,
this._toggleFavourite,
) : super(const FavouritesState());
}
Each app registers production implementations in its own dependency graph. This keeps environment configuration, HTTP clients, and app-specific services out of the shared package.
Use one public import#
Application code should import the package's public library:
import 'package:app_core/app_core.dart';
Avoid importing files from another package's src directory. That makes private folders an accidental API and prevents internal refactoring.
Barrel exports should remain intentional. Exporting the whole directory gives consumers access to implementation details that later become difficult to remove.
Know when duplication is acceptable#
Keep code app-local when:
- only one app uses it;
- the UX and rules differ materially;
- it is root navigation or bootstrap code;
- sharing would require many app checks;
- the abstraction is larger than the duplicated code.
Start with a real second consumer. Extracting a hypothetical shared feature too early usually produces the wrong interface.
Test at two levels#
The shared package should test business behavior, mapping, and BLoC transitions. Each app should keep smaller integration tests that prove:
- dependency registration succeeds;
- routes open the shared feature;
- app branding and wrappers are correct;
- app-specific callbacks reach the intended destination.
This split keeps core behavior tested once while still protecting the composition unique to each app.
The goal of a shared core is not maximum reuse. It is one trustworthy implementation of behavior that is genuinely the same, with each application remaining free to decide how that behavior enters its product.