Skip to content

Commit d1f74bb

Browse files
committed
feat: Add HeadlineItem and state widgets
- Added HeadlineItemWidget - Added FailureStateWidget - Added LoadingStateWidget - Added InitialStateWidget
1 parent 39e76b6 commit d1f74bb

File tree

6 files changed

+191
-3
lines changed

6 files changed

+191
-3
lines changed
Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,92 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:ht_main/headlines-feed/bloc/headlines_feed_bloc.dart';
4+
import 'package:ht_main/headlines-feed/widgets/headline_item_widget.dart';
5+
import 'package:ht_main/shared/widgets/failure_state_widget.dart';
6+
import 'package:ht_main/shared/widgets/initial_state_widget.dart';
7+
import 'package:ht_main/shared/widgets/loading_state_widget.dart';
28

3-
class HeadlinesFeedPage extends StatelessWidget {
9+
class HeadlinesFeedPage extends StatefulWidget {
410
const HeadlinesFeedPage({super.key});
511

12+
@override
13+
State<HeadlinesFeedPage> createState() => _HeadlinesFeedPageState();
14+
}
15+
16+
class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
17+
final _scrollController = ScrollController();
18+
19+
@override
20+
void initState() {
21+
super.initState();
22+
_scrollController.addListener(_onScroll);
23+
}
24+
25+
@override
26+
void dispose() {
27+
_scrollController
28+
..removeListener(_onScroll)
29+
..dispose();
30+
super.dispose();
31+
}
32+
33+
void _onScroll() {
34+
if (_isBottom) {
35+
context.read<HeadlinesFeedBloc>().add(HeadlinesFeedFetchRequested());
36+
}
37+
}
38+
39+
bool get _isBottom {
40+
if (!_scrollController.hasClients) return false;
41+
final maxScroll = _scrollController.position.maxScrollExtent;
42+
final currentScroll = _scrollController.offset;
43+
return currentScroll >= (maxScroll * 0.9);
44+
}
45+
646
@override
747
Widget build(BuildContext context) {
8-
return const Placeholder(); // Placeholder for now
48+
return Scaffold(
49+
appBar: AppBar(title: const Text('Headlines Feed')),
50+
body: BlocBuilder<HeadlinesFeedBloc, HeadlinesFeedState>(
51+
builder: (context, state) {
52+
switch (state) {
53+
case HeadlinesFeedInitial():
54+
return const InitialStateWidget();
55+
case HeadlinesFeedLoading():
56+
return const LoadingStateWidget();
57+
case HeadlinesFeedLoaded():
58+
return RefreshIndicator(
59+
onRefresh: () async {
60+
context
61+
.read<HeadlinesFeedBloc>()
62+
.add(HeadlinesFeedRefreshRequested());
63+
},
64+
child: ListView.builder(
65+
controller: _scrollController,
66+
itemCount: state.hasMore
67+
? state.headlines.length + 1
68+
: state.headlines.length,
69+
itemBuilder: (context, index) {
70+
if (index >= state.headlines.length) {
71+
return const LoadingStateWidget();
72+
}
73+
final headline = state.headlines[index];
74+
return HeadlineItemWidget(headline: headline);
75+
},
76+
),
77+
);
78+
case HeadlinesFeedError():
79+
return FailureStateWidget(
80+
message: state.message,
81+
onRetry: () {
82+
context
83+
.read<HeadlinesFeedBloc>()
84+
.add(HeadlinesFeedRefreshRequested());
85+
},
86+
);
87+
}
88+
},
89+
),
90+
);
991
}
1092
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:ht_headlines_repository/ht_headlines_repository.dart'
3+
show Headline;
4+
5+
/// A widget that displays a single headline.
6+
class HeadlineItemWidget extends StatelessWidget {
7+
/// Creates a [HeadlineItemWidget].
8+
const HeadlineItemWidget({required this.headline, super.key});
9+
10+
/// The headline to display.
11+
final Headline headline;
12+
13+
@override
14+
Widget build(BuildContext context) {
15+
return Padding(
16+
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
17+
child: Column(
18+
crossAxisAlignment: CrossAxisAlignment.start,
19+
children: [
20+
Text(
21+
headline.title ?? 'No Title',
22+
style: Theme.of(context).textTheme.titleMedium,
23+
),
24+
if (headline.description != null)
25+
Padding(
26+
padding: const EdgeInsets.only(top: 4),
27+
child: Text(
28+
headline.description!,
29+
style: Theme.of(context).textTheme.bodySmall,
30+
),
31+
),
32+
],
33+
),
34+
);
35+
}
36+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'package:flutter/material.dart';
2+
3+
/// A widget to display an error message and an optional retry button.
4+
class FailureStateWidget extends StatelessWidget {
5+
/// Creates a [FailureStateWidget].
6+
///
7+
/// The [message] is the error message to display.
8+
/// The [onRetry] is an optional callback to be called when the retry button is pressed.
9+
const FailureStateWidget({
10+
required this.message, super.key,
11+
this.onRetry,
12+
});
13+
14+
/// The error message to display.
15+
final String message;
16+
17+
/// An optional callback to be called when the retry button is pressed.
18+
final VoidCallback? onRetry;
19+
20+
@override
21+
Widget build(BuildContext context) {
22+
return Center(
23+
child: Column(
24+
mainAxisAlignment: MainAxisAlignment.center,
25+
children: [
26+
Text(
27+
message,
28+
style: Theme.of(context).textTheme.bodyMedium,
29+
textAlign: TextAlign.center,
30+
),
31+
// Show the retry button only if onRetry is provided
32+
if (onRetry != null)
33+
Padding(
34+
padding: const EdgeInsets.only(top: 16),
35+
child: ElevatedButton(
36+
onPressed: onRetry,
37+
child: const Text('Retry'),
38+
),
39+
),
40+
],
41+
),
42+
);
43+
}
44+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import 'package:flutter/material.dart';
2+
3+
class InitialStateWidget extends StatelessWidget {
4+
const InitialStateWidget({super.key});
5+
6+
@override
7+
Widget build(BuildContext context) {
8+
return const Center(
9+
child: Text('Initial State'),
10+
);
11+
}
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'package:flutter/material.dart';
2+
3+
class LoadingStateWidget extends StatelessWidget {
4+
const LoadingStateWidget({super.key});
5+
6+
@override
7+
Widget build(BuildContext context) {
8+
return Center(
9+
child: CircularProgressIndicator(
10+
color: Theme.of(context).colorScheme.secondary,
11+
),
12+
);
13+
}
14+
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: ht_main
22
description: main headlines toolkit mobile app.
3-
version: 0.6.5
3+
version: 0.7.5
44
publish_to: none
55
repository: https://github.com/headlines-toolkit/ht-main
66
environment:

0 commit comments

Comments
 (0)