Skip to content

Instantly share code, notes, and snippets.

@rafdls
Last active August 10, 2020 05:39
Show Gist options
  • Save rafdls/9f51bec2500fb65096170d4350e96cc9 to your computer and use it in GitHub Desktop.
Save rafdls/9f51bec2500fb65096170d4350e96cc9 to your computer and use it in GitHub Desktop.
How to make an online radio app in Flutter part 2
BlocBuilder<StationsBloc, StationsState>(
condition: (context, state) {
return (state is! FetchingNextStationsState);
},
builder: (context, state) {
if (state is InitialState) {
//Send fetch stations event
} else if (state is LoadingStationsState) {
//Display Loading widget
} else if (state is StationsFetchedState) {
//Use stations data from StationsFetchedState and show stations list widget
} else {
//Show error screen
}
},
);
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Online Radio'),
),
body: BlocBuilder<StationsBloc, StationsState>(
condition: (context, state) {
return (state is! FetchingNextStationsState);
},
builder: (context, state) {
if (state is InitialState) {
context.bloc<StationsBloc>().add(FetchStations());
return SizedBox();
} else if (state is LoadingStationsState) {
return LoadingIndicatorWithMessage(label: 'Fetching stations');
} else if (state is StationsFetchedState) {
final stations = state.stations;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TitleHeader(
title: 'Top Stations',
status: BlocBuilder<PlayerBloc, PlayerState>(builder: (context, state) {
if (state is PausedState || state is StoppedState) {
return PausedStatus();
} else {
return PlayingStatus();
}
}),
),
Expanded(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (state is StationsFetchedState &&
scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
context.bloc<StationsBloc>().add(FetchNextStations());
return true;
} else {
return false;
}
},
child: ListView.builder(
itemBuilder: (context, index) {
if (index < stations.length) {
return StationListItem(
name: stations[index].name,
imageUrl: stations[index].imageUrl,
onTap: () {
context.bloc<PlayerBloc>().add(PlayEvent(stations[index]));
},
);
} else if (index == stations.length && !state.hasFetchedAll) {
return Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 4),
child: CircularProgressIndicator(),
),
);
} else {
return null;
}
},
),
),
),
BlocBuilder<PlayerBloc, PlayerState>(
builder: (context, state) {
if (state is! StoppedState) {
return SizedBox(height: 80);
} else {
return SizedBox();
}
},
)
],
);
} else {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('An error has occurred'),
SizedBox(height: 20),
MaterialButton(
onPressed: () {
context.bloc<StationsBloc>().add(FetchStations());
},
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).accentColor,
child: Text('Retry'),
)
],
),
);
}
},
),
bottomSheet: BlocBuilder<PlayerBloc, PlayerState>(
builder: (context, state) {
if (state is StoppedState) {
return SizedBox();
} else if (state is PlayingState) {
final currentStation = state.currentStation;
return MediaPlayerSheet(
title: currentStation.name,
imageUrl: currentStation.imageUrl,
mediaButtonIcon: Icon(
Icons.pause,
size: 32,
),
onMediaButtonPress: () {
context.bloc<PlayerBloc>().add(PauseEvent());
},
);
} else {
final currentStation = (state as PausedState).currentStation;
return MediaPlayerSheet(
title: currentStation.name,
imageUrl: currentStation.imageUrl,
mediaButtonIcon: Icon(
Icons.play_arrow,
size: 32,
),
onMediaButtonPress: () {
context.bloc<PlayerBloc>().add(PlayEvent(currentStation));
},
);
}
},
),
);
}
}
context.bloc<StationsBloc>().add(FetchStations());
return SizedBox();
return LoadingIndicatorWithMessage(label: 'Fetching stations');
dependencies:
flutter:
sdk: flutter
bloc: ^3.0.0
flutter_bloc: ^3.2.0
just_audio: ^0.1.3
loading: ^1.0.2
cupertino_icons: ^0.1.2
//New Dependencies
dio: ^3.0.9
dio_http_cache: ^0.2.6
cached_network_image: ^2.0.0
flutter_svg: ^0.18.0
class RadioApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final RadioPlayer radioPlayer = JustAudioPlayer();
final StationRepository stationRepository = RadioBrowserRepository(Dio());
return MaterialApp(
title: 'Online Radio',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: MultiBlocProvider(
providers: [
BlocProvider<PlayerBloc>(
create: (context) => PlayerBloc(radioPlayer: radioPlayer),
),
BlocProvider<StationsBloc>(
create: (context) => StationsBloc(stationRepository: stationRepository),
),
],
child: HomeScreen(),
),
);
}
}
class RadioBrowserRepository extends StationRepository {
final Dio _dio;
static final String _baseUrl = 'https://fr1.api.radio-browser.info';
static final String _stationsByCountryCodeUrl = '$_baseUrl/json/stations/bycountrycodeexact/';
RadioBrowserRepository(this._dio) {
_dio.interceptors.add(DioCacheManager(CacheConfig(baseUrl: _baseUrl)).interceptor);
}
@override
Future<List<Station>> getStationsByCountryPaginated(
String countryCode,
int offset,
int limit,
) async {
final stationsFromCountryCodeUrl = _stationsByCountryCodeUrl + countryCode;
final Response rawStationsJson = await _dio.get(
_buildUrlToSortByPopularityWithPagination(stationsFromCountryCodeUrl, offset, limit),
options: buildCacheOptions(
Duration(days: 1),
),
);
final List<Station> stations = (rawStationsJson.data as List)
.map((responseJson) => Station(
responseJson['url_resolved'],
responseJson['favicon'],
responseJson['name'],
))
.toList();
return Future.value(stations);
}
String _buildUrlToSortByPopularityWithPagination(String url, int offset, int limit) {
return '$url?hidebroken=true&order=clickcount&reverse=true&offset=$offset&limit=$limit';
}
}
class Station extends Equatable {
final String radioUrl;
final String imageUrl;
final String name;
Station(this.radioUrl, this.imageUrl, this.name);
@override
List<Object> get props => [radioUrl, imageUrl, name];
}
class StationsBloc extends Bloc<StationsEvent, StationsState> {
final StationRepository stationRepository;
final int _pageSize = 15;
final String _countryCode = 'au';
StationsBloc({@required this.stationRepository}) : assert(stationRepository != null);
@override
StationsState get initialState => LoadingStationsState();
@override
Stream<StationsState> mapEventToState(
StationsEvent event,
) async* {
if (event is FetchStations) {
yield (LoadingStationsState());
try {
final List<Station> stations =
await stationRepository.getStationsByCountryPaginated(_countryCode, 0, _pageSize);
yield StationsFetchedState(
stations: stations,
stationPageIndex: 0,
hasFetchedAll: false,
);
} catch (err) {
yield StationsFetchErrorState();
}
} else if (event is FetchNextStations) {
if (state is StationsFetchedState) {
final currentState = (state as StationsFetchedState);
final int index = currentState.stationPageIndex + _pageSize;
final List<Station> oldStations = currentState.stations;
yield FetchingNextStationsState();
try {
final List<Station> stations =
await stationRepository.getStationsByCountryPaginated(_countryCode, index, _pageSize);
yield StationsFetchedState(
stations: oldStations..addAll(stations),
stationPageIndex: index,
hasFetchedAll: (stations.length < _pageSize) ? true : false,
);
} catch (err) {
yield StationsFetchErrorState();
}
}
}
}
}
BlocBuilder<PlayerBloc, PlayerState>(
builder: (context, state) {
if (state is! StoppedState) {
return SizedBox(height: 80);
} else {
return SizedBox();
}
},
);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('An error has occurred'),
SizedBox(height: 20),
MaterialButton(
onPressed: () {
context.bloc<StationsBloc>().add(FetchStations());
},
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).accentColor,
child: Text('Retry'),
)
],
),
);
@immutable
abstract class StationsEvent {}
class FetchStations extends StationsEvent {}
class FetchNextStations extends StationsEvent {}
final stations = state.stations;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TitleHeader(/*contains logic to say if radio is playing or not*/),
Expanded(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
//Send FetchNextStationsEvent()
},
child: ListView.builder(
itemBuilder: (context, index) {
if (index < stations.length) {
//Return StationListItem()
} else if (index == stations.length && !state.hasFetchedAll) {
//Return LoadingSpinnerItem()
} else {
return null;
}
},
),
),
),
BlocBuilder<PlayerBloc, PlayerState>(
builder: (context, state) {
if (state is! StoppedState) {
return SizedBox(height: 80);
} else {
return SizedBox();
}
},
),
],
);
}
ListView.builder(
itemBuilder: (context, index) {
if (index < stations.length) {
return StationListItem(
name: stations[index].name,
imageUrl: stations[index].imageUrl,
onTap: () {
context.bloc<PlayerBloc>().add(PlayEvent(stations[index]));
},
);
} else if (index == stations.length && !state.hasFetchedAll) {
return Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 4),
child: CircularProgressIndicator(),
),
);
} else {
return null;
}
},
);
@immutable
abstract class StationsState {
const StationsState();
}
class InitialState extends StationsState {}
class LoadingStationsState extends StationsState {}
class FetchingNextStationsState extends StationsState {}
class StationsFetchedState extends StationsState {
final List<Station> stations;
final int stationPageIndex;
final bool hasFetchedAll;
const StationsFetchedState({
@required this.stations,
@required this.stationPageIndex,
@required this.hasFetchedAll,
}) : assert(stations != null && stationPageIndex != null && hasFetchedAll != null);
}
class StationsFetchErrorState extends StationsState {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment