| j / k | Scroll down / up |
| gg | Scroll to top |
| G | Scroll to bottom |
| ⌘K / / | Open search |
| ? | Show this help |
| Esc | Close search / help |
| n / N | Next / previous section |
| h | Go to landing |
| d | Go to docs |
| : | Command mode — type section name |
Press ? or Esc to close
Forms are plain Cubits — no form library. Field state lives in the Cubit; widgets are stateless and read/write through it.
Each form has a state holding the editable fields plus an ApiResponse for the submit status. A separate validation method returns the first error.
class LoginState {
final String email;
final String password;
final SimpleApiResponse<void> submit;
const LoginState({this.email = '', this.password = '', this.submit = const ApiResponse.initial()});
LoginState copyWith({String? email, String? password, SimpleApiResponse<void>? submit}) =>
LoginState(email: email ?? this.email, password: password ?? this.password, submit: submit ?? this.submit);
} class LoginCubit extends Cubit<LoginState> {
LoginCubit(this._store) : super(const LoginState());
void emailChanged(String v) => emit(state.copyWith(email: v));
void passwordChanged(String v) => emit(state.copyWith(password: v));
Future<void> submit() async {
emit(state.copyWith(submit: state.submit.loading));
final res = await _store.login(state.email, state.password);
emit(state.copyWith(submit: res));
}
} Use AppTextField for text entry and AppButton for actions. Both read design tokens — never raw colors or padding.
AppTextField(
value: state.email,
onChanged: cubit.emailChanged,
hint: context.l10n.t(TranslationKeys.authEmail),
);
AppButton(
label: context.l10n.t(TranslationKeys.authSignIn),
isLoading: state.submit.isLoading,
onPressed: cubit.submit,
); | Case | Pattern |
|---|---|
| Field validation | Inline, under the field — ErrorScope.local |
| Submit failure | Snackbar via GlobalErrorCubit |
| Disabled button | Derive from state.submit.isLoading |