refactor: convert BillingView to StatefulWidget and implement scroll listener for dynamic UI updates

This commit is contained in:
Achintha Isuru
2026-01-30 00:40:19 -05:00
parent bfe00a700a
commit 4fce8f9a57

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -6,7 +7,6 @@ import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/billing_bloc.dart';
import '../blocs/billing_event.dart';
import '../blocs/billing_state.dart';
import '../widgets/billing_header.dart';
import '../widgets/invoice_history_section.dart';
import '../widgets/payment_method_card.dart';
import '../widgets/pending_invoices_section.dart';
@@ -34,23 +34,136 @@ class BillingPage extends StatelessWidget {
///
/// This widget displays the billing dashboard content based on the current
/// state of the [BillingBloc].
class BillingView extends StatelessWidget {
class BillingView extends StatefulWidget {
/// Creates a [BillingView].
const BillingView({super.key});
@override
State<BillingView> createState() => _BillingViewState();
}
class _BillingViewState extends State<BillingView> {
late ScrollController _scrollController;
bool _isScrolled = false;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.hasClients) {
if (_scrollController.offset > 140 && !_isScrolled) {
setState(() => _isScrolled = true);
} else if (_scrollController.offset <= 140 && _isScrolled) {
setState(() => _isScrolled = false);
}
}
}
@override
Widget build(BuildContext context) {
return BlocBuilder<BillingBloc, BillingState>(
builder: (BuildContext context, BillingState state) {
return Scaffold(
body: Column(
children: <Widget>[
BillingHeader(
currentBill: state.currentBill,
savings: state.savings,
onBack: () => Modular.to.pop(),
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: 200.0,
backgroundColor: UiColors.primary,
leading: Center(
child: UiIconButton.secondary(
icon: UiIcons.arrowLeft,
onTap: () => Modular.to.pop(),
),
),
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(
_isScrolled
? '\$${state.currentBill.toStringAsFixed(2)}'
: t.client_billing.title,
key: ValueKey<bool>(_isScrolled),
style: UiTypography.headline4m.copyWith(
color: UiColors.white,
),
),
),
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: const EdgeInsets.only(
top: UiConstants.space0,
left: UiConstants.space5,
right: UiConstants.space5,
bottom: UiConstants.space10,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text(
t.client_billing.current_period,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.7),
),
),
const SizedBox(height: UiConstants.space1),
Text(
'\$${state.currentBill.toStringAsFixed(2)}',
style: UiTypography.display1b
.copyWith(color: UiColors.white),
),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: UiConstants.space1,
),
decoration: BoxDecoration(
color: UiColors.accent,
borderRadius: BorderRadius.circular(100),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(
UiIcons.trendingDown,
size: 12,
color: UiColors.foreground,
),
const SizedBox(width: UiConstants.space1),
Text(
t.client_billing.saved_amount(
amount: state.savings.toStringAsFixed(0),
),
style: UiTypography.footnote2b.copyWith(
color: UiColors.foreground,
),
),
],
),
),
],
),
),
),
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
_buildContent(context, state),
],
),
),
Expanded(child: _buildContent(context, state)),
],
),
);
@@ -60,7 +173,10 @@ class BillingView extends StatelessWidget {
Widget _buildContent(BuildContext context, BillingState state) {
if (state.status == BillingStatus.loading) {
return const Center(child: CircularProgressIndicator());
return const Padding(
padding: EdgeInsets.all(UiConstants.space10),
child: Center(child: CircularProgressIndicator()),
);
}
if (state.status == BillingStatus.failure) {
@@ -72,7 +188,7 @@ class BillingView extends StatelessWidget {
);
}
return SingleChildScrollView(
return Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -89,6 +205,7 @@ class BillingView extends StatelessWidget {
const SizedBox(height: UiConstants.space6),
InvoiceHistorySection(invoices: state.invoiceHistory),
const SizedBox(height: UiConstants.space24),
SizedBox(height: MediaQuery.of(context).size.height * 0.8),
],
),
);