electrical-engineering-principles
Implementing Solid Principles in Cross-platform Mobile Apps
Table of Contents
Why SOLID Principles Matter in Cross-Platform Mobile Development
Building mobile apps that run seamlessly across iOS and Android is no small feat. Cross-platform frameworks such as React Native, Flutter, and .NET MAUI promise code reuse, but without a solid architectural foundation, shared codebases quickly become tangled, brittle, and difficult to evolve. The SOLID principles offer a time-tested blueprint for designing object-oriented systems that are resilient to change. By applying these five principles to cross-platform projects, developers can create apps that are not only easier to maintain but also more performant and adaptable to platform-specific requirements.
In this expanded guide, we’ll dive deeper into each SOLID principle, provide concrete implementation examples for popular cross-platform frameworks, and discuss how to overcome common pitfalls when sharing logic across platforms.
Understanding the SOLID Principles
SOLID is an acronym that encapsulates five fundamental design principles originally introduced by Robert C. Martin. They guide developers in creating software that is flexible, reusable, and maintainable. Below is a quick overview before we examine each principle in detail.
- Single Responsibility Principle (SRP) – A class should have only one reason to change.
- Open/Closed Principle (OCP) – Software entities should be open for extension but closed for modification.
- Liskov Substitution Principle (LSP) – Subtypes must be substitutable for their base types.
- Interface Segregation Principle (ISP) – No client should be forced to depend on methods it does not use.
- Dependency Inversion Principle (DIP) – Depend on abstractions, not on concretions.
Why SOLID Matters in Cross-Platform Development
Cross-platform apps introduce unique complexity: shared business logic must coexist with platform-specific UI code, APIs, and device features. Without clear boundaries, the same class might handle network requests, data caching, and UI updates – creating a maintenance nightmare. SOLID principles help enforce separation of concerns, making it possible to:
- Reuse core logic across platforms with minimal changes.
- Swap out platform-specific implementations (e.g., file storage, biometrics) without touching shared code.
- Write unit tests for business logic independent of UI frameworks.
- Onboard new developers faster by providing a predictable module structure.
Let’s now explore how each principle can be applied effectively in frameworks like Flutter, React Native, and .NET MAUI.
Single Responsibility Principle (SRP)
SRP states that every class or module should have a single, well-defined responsibility. This is often paraphrased as “a class should have only one reason to change.” In cross-platform apps, SRP is especially critical because platform-specific logic (like accessing the camera) must be isolated from business rules (like processing a photo).
Example in Flutter
Consider a user profile screen. A common violation is to have a single UserProfileWidget that fetches data, parses it, and displays the UI. Instead, separate the concerns:
- Data layer: A
UserRepositoryclass that only handles fetching and caching user data. - Business logic: A
UserProfileViewModelthat transforms raw data into displayable state. - UI layer: A stateless widget that renders the state.
This way, if the API endpoint changes, only the repository is modified. If the UI design changes, the widget is updated in isolation.
Example in React Native
In React Native, you might be tempted to put all logic inside a component using hooks. Instead, extract data fetching into a custom hook (e.g., useUserData) and keep the component focused solely on rendering. For more complex apps, use a state management library like Redux or MobX with dedicated action creators and reducers.
Common Pitfall
Developers often confuse “responsibility” with “functionality.” A class that formats dates and also validates user input has two responsibilities. SRP urges you to split such tasks into distinct utilities.
Open/Closed Principle (OCP)
The Open/Closed Principle suggests that classes should be open for extension but closed for modification. This means you should be able to add new functionality without altering existing code – typically achieved via inheritance or by implementing interfaces.
Cross-Platform Application
Imagine you need to support different push notification providers (Firebase, APNs, etc.). Instead of modifying a central NotificationService every time a new provider is added, define an abstract PushNotificationProvider interface. Each provider implements the interface, and the app uses a factory or dependency injection to select the appropriate one. Existing code remains untouched.
Example in .NET MAUI
Using .NET MAUI, you can define an interface ILocalStorage with methods like SaveToken and GetToken. Platform-specific implementations (iOSLocalStorage, AndroidLocalStorage) are registered via dependency injection at startup. When a new platform (e.g., Windows) is added, you simply create a new implementation without touching the shared logic.
Testing OCP Compliance
Write tests that verify you can add new extensions (e.g., a new sort algorithm) without requiring changes to existing unit tests. If you must modify existing tests, your design likely violates OCP.
Liskov Substitution Principle (LSP)
LSP ensures that derived classes can replace base classes without altering the correctness of the program. This principle is often violated when subclasses weaken the preconditions or strengthen the postconditions of the base class.
Real-World Example
Suppose you have a base class StorageService that promises to save a file and return a boolean indicating success. A subclass EncryptedStorageService might require an encryption key at construction time – a stricter precondition. If the client code expects to instantiate any StorageService without a key, the substitution fails. To respect LSP, either make the key part of the base class contract or use composition instead of inheritance.
Cross-Platform Concerns
In cross-platform development, platform-specific subclasses often behave differently. For example, a CameraService might return a photo on iOS but throw an exception on Android because permissions weren’t granted. To honor LSP, define a common result type (e.g., Result) so that all implementations return the same contract. This allows the shared code to handle all outcomes uniformly.
Interface Segregation Principle (ISP)
ISP states that interfaces should be focused and small, tailored to specific clients. A monolithic interface forces implementing classes to provide dummy or empty implementations for methods they don’t need – a clear sign of poor abstraction.
Example in Flutter
Consider an interface for user interactions: UserActions with methods login, logout, register, resetPassword, and deleteAccount. A guest user might only need login and register. Breaking this into smaller interfaces like AuthenticationActions and AccountManagementActions allows classes to implement only what they use. In practice, you might have separate repository classes for authentication and account management.
React Native Approach
In React Native, this translates to creating multiple custom hooks or service objects that each handle a narrow slice of functionality. Instead of a single useUser hook that returns everything, split into useAuth, useProfile, and useSettings hooks. Components then choose exactly what they need.
ISP and Cross-Platform APIs
When writing platform-specific code, resist the urge to create one massive “PlatformBridge” class. Instead, define separate interfaces for camera, GPS, notifications, etc. This makes it easier to test mock implementations and swap out platforms.
Dependency Inversion Principle (DIP)
DIP encourages depending on abstractions (interfaces or abstract classes) rather than concrete implementations. This decouples high-level modules from low-level details, making the system more flexible and testable.
Dependency Injection in Practice
The most common way to implement DIP is through dependency injection (DI). In Flutter, popular DI containers like get_it or Provider allow you to register abstractions and resolve them at runtime. For example:
- Define
IHttpClientinterface. - Implement
DioHttpClient(Dio) for production. - Implement
MockHttpClientfor testing. - Register the appropriate implementation based on environment.
The UserRepository class depends only on IHttpClient, not on the concrete implementation. This allows you to run unit tests without network calls and switch HTTP libraries by merely writing a new implementation.
Platform-Specific Dependencies
In cross-platform apps, DIP shines when dealing with platform-specific services. For instance, a IBiometricAuth interface can have an Android implementation using Android Biometric API and an iOS implementation using Local Authentication framework. The shared business logic calls IBiometricAuth without knowing which platform it’s running on.
Testing Benefits
With DIP, you can inject mocks for all external dependencies in unit tests, ensuring that your business logic is tested in isolation. This is a huge advantage in cross-platform development where simulator/emulator behavior can differ from real devices.
Practical Implementation Strategies
Knowing the principles is one thing; applying them consistently requires a deliberate architecture. Below are strategies to embed SOLID into your cross-platform development workflow.
Use Clean Architecture or Similar Patterns
Clean Architecture, as described by Robert C. Martin, inherently follows SOLID principles. It separates the codebase into layers: domain (entities & use cases), data (repositories & data sources), and presentation (UI layer). Each layer communicates via abstractions, making it easy to apply DIP and ISP.
Leverage Dependency Injection Containers
Modern cross-platform frameworks offer built-in or third-party DI. For .NET MAUI, use Microsoft.Extensions.DependencyInjection. For Flutter, use get_it or riverpod. For React Native, you can use libraries like tsyringe or leverage the module system with explicit dependency passing.
Isolate Platform-Specific Code
Use the Abstract Factory pattern to create platform-specific implementations behind a common interface. This respects OCP and helps you abide by LSP, as all factories return objects that adhere to the same contract.
Write Unit Tests First
Test-driven development (TDD) naturally enforces SOLID. When you write tests for a class, you quickly notice if it has too many responsibilities (violating SRP) or if you need to instantiate many concrete dependencies (violating DIP). Let your tests drive you toward cleaner abstractions.
Real-World Benefits of SOLID Compliance
Teams that adopt SOLID principles in cross-platform projects report measurable improvements:
- Reduced bug rate – Changes are localized, so fixing one feature rarely breaks another.
- Faster feature development – New features are added by extending existing abstractions rather than rewriting entire modules.
- Easier onboarding – New developers can focus on one small interface or class at a time, understanding the system incrementally.
- Better test coverage – Decoupled code is inherently more testable, leading to higher confidence in cross-platform behavior.
Performance Considerations
A common misconception is that abstracting too much hurts performance. In practice, the overhead of interface dispatch in modern runtimes (Flutter’s Dart, .NET, React Native’s JavaScript) is negligible. The maintainability gains far outweigh any micro-performance costs.
Conclusion
Applying SOLID principles to cross-platform mobile development is more than a theoretical exercise – it’s a practical strategy for building apps that can evolve with changing requirements and platform updates. By enforcing single responsibilities, designing for extension, ensuring substitutability, segregating interfaces, and inverting dependencies, you create a codebase that is modular, testable, and resilient. Start small: refactor one module at a time, introduce DI gradually, and watch your team’s productivity and code quality soar. The upfront investment in design yields dividends across the entire app lifecycle, from initial development to long-term maintenance. For further reading, explore Martin Fowler’s article on Dependency Injection and the official SOLID principles documentation in your chosen framework’s ecosystem.