Flutter Feature Development Order

 ・ 6 min

photo by Valeria Kodra on Unsplash

I'm going to organize the order of developing features in Flutter.
After Feature Definition and Requirements Analysis, a rough screen layout should be ready, and before UI and UX Design, the screen design needs to be finalized so you can actually start building the screens.

Development Order#

1. Feature Definition and Requirements Analysis#

  • First, clearly define how a specific feature (a single flow) should work.
  • For example, if it's appointment creation, you'd create a feature where users can enter an appointment title, date, time, location, etc. and save it.

2. Domain Model Definition#

  • Define a rough domain model that can support the feature. For appointments, you might need models for storing input values and models for data that needs to be processed in the UI — multiple models may be created for a single feature.
  • Models encapsulate the core business logic and data of the application in the domain layer.
class Appointment {
  final String title;
  final DateTime date;
  final String location;
 
  Appointment({
    required this.title,
    required this.date,
    required this.location,
  });
}

3. Repository Pattern Implementation#

  • Design a Repository to handle storing and retrieving domain models.
  • For example, implement logic to save and retrieve appointment data from a local DB or remote server.
abstract class AppointmentRepository {
  Future<void> saveAppointment(Appointment appointment);
  Future<List<Appointment>> getAppointments();
}
  • At this point, write concrete implementations like LocalAppointmentRepository or RemoteAppointmentRepository that handle the actual data source.
class LocalAppointmentRepository implements AppointmentRepository {
  @override
  Future<void> saveAppointment(Appointment appointment) async {
    // Local storage logic here
  }
 
  @override
  Future<List<Appointment>> getAppointments() async {
    // Retrieve data from local storage
  }
}

4. Application Service Layer Definition#

  • Define service classes that perform business logic using domain models and repositories.
  • This layer encapsulates business logic like appointment creation so it can be easily called from the Presentation Layer. This isn't always the case, but it can be good to enforce that domains are only created here. For data retrieval, this might not apply since the domain already exists.
class CreateAppointmentService {
  final AppointmentRepository repository;
 
  CreateAppointmentService(this.repository);
 
  Future<void> createAppointment(String title, DateTime date, String location) async {
    final appointment = Appointment(title: title, date: date, location: location);
    await repository.saveAppointment(appointment);
  }
}

5. Presentation Layer Design#

  • The presentation layer manages UI and application state.
  • Use Riverpod to manage state and design the UI for user interaction.
  • Repository and service also use Riverpod.
part 'appointment_controller.g.dart';
 
@riverpod
class AppointmentController extends _$AppointmentController {
  @override
  Appointment build() async {
	  return Appointment.empty();
  }
 
Future<Appointment> createAppointment(String title, DateTime date, String location) {
	state = state.copyWith(title: title, date: date, location: location);
     final service = ref.read(createAppointmentServiceProvider);
     final appointment = await service.createAppointment(title, date, location);
	 return appointment;
  }
}
  • Use this appointmentControllerProvider in the UI to read state and create appointments.
class CreateAppointmentScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final appointment = ref.watch(appointmentControllerProvider);
 
    return Scaffold(
      appBar: AppBar(title: Text('Create Appointment')),
      body: Column(
        children: [
          // Input fields for title, date, location...
          ElevatedButton(
            onPressed: () {
              // Trigger createAppointment
            },
            child: Text('Save Appointment'),
          ),
        ListTile(
			  title: Text(appointment.title),
			  subtitle: Text('${appointment.date} at ${appointment.location}'),
          ),
        ],
      ),
    );
  }
}

6. UI and UX Design#

  • Design the UI based on the presentation layer above and implement the necessary forms and widgets.
  • Complete the final UI through screen transitions, input validation, usability testing, etc.
  • For certain widgets in the UI, there are states like normal, loading, success, error, and empty, so you need to decide how each state is handled and displayed.

7. Writing Tests#

  • Write unit tests and integration tests for each layer you've built (domain model, repository, service, presentation).
  • Using TDD to test at each stage is also a good approach.
  • Verify through testing that the application works as intended.

8. Integration and Optimization#

  • Combine all components to test the complete feature, and perform optimization as needed.
  • Merge with the main branch and release a new version through deployment automation.

Summary#

  1. Feature definition and domain model design.
  2. Data storage and management through the repository pattern.
  3. Business logic implementation in the application service layer.
  4. State management and UI implementation in the presentation layer with Riverpod.
  5. UI design and UX optimization.
  6. Test writing and verification.

Helpful things to know alongside the development order:
Adopting Agile methods: Set sprints -> Focus on specific features for a set period -> Break large projects into smaller units, set priorities.
Using TDD: Write test cases before writing code -> Write code to pass the tests
Adopting DevOps: Code review, test automation & CI/CD

State Management#

State management: A way to maintain consistency between UI and data (state). Whenever data (state) changes, the UI updates accordingly so the user can see the latest information. The reason we use state management libraries is for code convenience and rendering efficiency.

Async requests, caching, error and loading state handling affect the following features:

  • Pull to refresh
  • Infinite lists / fetch on scroll
  • Search while typing
  • Debouncing async requests
  • Canceling async requests when no longer needed
  • Optimistic UI
  • Offline mode
  • ...

Avoid using Riverpod for simple state management. Riverpod recommends using it only for business logic as a suitable state management use case. Use Riverpod when you want to separate UI logic and state management.
Think of Riverpod as something you use for handling data that needs error and loading states, and you can use it in a more focused way.

Don't use providers for local widget state
Providers are designed for shared business state.

They're not suitable for use cases like:

  • Form state storage
  • Currently selected item
  • Animations
  • Everything Flutter typically handles with "controllers" (e.g., TextEditingController)
    If you're looking for a way to handle local widget state, consider using flutter_hooks instead.

One of the reasons this is discouraged is that such state is often scoped to a route. If you don't do this, a new page can override the previous page's state, potentially breaking your app's back button.


You have to give up some of the old so that you can make room for the new.

— Yanni


Other posts
Accessing localhost from iPhone 커버 이미지
 ・ 2 min

Accessing localhost from iPhone

Understanding Flutter Rendering Errors 커버 이미지
 ・ 9 min

Understanding Flutter Rendering Errors

Recommended VS Code Extensions 커버 이미지
 ・ 5 min

Recommended VS Code Extensions