Last active
August 10, 2020 05:39
-
-
Save rafdls/9f51bec2500fb65096170d4350e96cc9 to your computer and use it in GitHub Desktop.
How to make an online radio app in Flutter part 2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
}, | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
}, | |
); | |
} | |
}, | |
), | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
context.bloc<StationsBloc>().add(FetchStations()); | |
return SizedBox(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
return LoadingIndicatorWithMessage(label: 'Fetching stations'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | |
), | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
BlocBuilder<PlayerBloc, PlayerState>( | |
builder: (context, state) { | |
if (state is! StoppedState) { | |
return SizedBox(height: 80); | |
} else { | |
return SizedBox(); | |
} | |
}, | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | |
) | |
], | |
), | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@immutable | |
abstract class StationsEvent {} | |
class FetchStations extends StationsEvent {} | |
class FetchNextStations extends StationsEvent {} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
}, | |
), | |
], | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
}, | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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