This content originally appeared on DEV Community and was authored by BookOfCooks
I’m the developer of a family project, Prayershub, that aims to enhance worship services for families and church alike. At the core of the app is the Worship, which is a playlist of songs, bible verses, and prayers.
The app is now several years old, and the entire “Worship” section of the app is now a couple thousand LOC of tightly coupled RxDart streams, which I have to maintain.
As a way of keeping my sanity, I’ve decided to start write down approaches I’m taking to help keep the app more maintainable and make it easier to introduce new features into.
Today, I’m trying:
Bundle multiple streams into classes
Nothing too fancy or complicated, just basic refactoring to start with.
Above is a picture of the app. The “Play” button at the bottom of the screen will, upon being pressed, play each item in the worship one-after-another.
The black play button next to each item is a solo play button. It will only play that single item.
That single button combines 4 streams:
-
Stream<bool>
running$: Is the worship playing (either solo or personal) -
Stream<int?>
soloing$: the current item being soloed (if null, we’re not in solo mode) -
Stream<bool>
audio.playingStream: is the worship_audio_file (a single mp3 file that’s the concatenation of all items (oversimplified)) playing? -
Stream<bool>
soloAudio.playingStream: is the solo_audio_file (aAudioPlayer
who’ssrc
changes if an item is picked for solo) playing?
Here’s the resulting code
StreamToWidget( // custom wrapper around StreamBuilder
// --- the stream
Rx.combineLatest4(
running,
soloing$,
audio.playingStream,
soloAudio.playingStream,
(running, soloing, playing, soloAudioPlaying) {
return (
running: running,
soloing: soloing,
playing: playing,
soloAudioPlaying: soloAudioPlaying,
);
},
),
// --- the converter
converter: (data) {
bool isActive = data.soloing == contentIndex;
bool isPaused = isActive && !data.playing;
if (soloMap != null && soloMap![content.id] != null) {
isPaused = isActive && !data.soloAudioPlaying;
}
if (data.running && data.soloing == null) {
return const SizedBox();
}
return SoloButton(
...
);
},
)
Let’s focus specifically on Rx.combineLatest4
. This is a stream that combines multiple streams, and I’ve used this in many places across the app.
The BottomAppBar
that contains all the buttons for controlling the playlist, combines a total of 7 streams!
StreamBuilder(
stream: CombineLatestStream.combine7(
contents$,
running,
audio.playingStream,
soloAudio.playingStream,
editing,
soloing$,
showCountdown,
(a, b, c, d, e, f, g) => (
contents: a,
running: b,
playing: c,
soloPlaying: d,
editing: e,
soloing: f,
showCountdown: g
),
),
initialData: (
contents: <WorshipContent>[],
running: false,
playing: false,
soloPlaying: false,
editing: false,
soloing: null,
showCountdown: false,
),
builder: (context, snapshot) {
var (
:contents,
:running,
:playing,
:soloPlaying,
:editing,
:soloing,
:showCountdown
) = snapshot.requireData;
if (contents.isEmpty || editing || showCountdown) {
return const SizedBox.shrink();
}
// ... lots of code
},
)
I decided to factor the resulting data of the stream into an actual class WorshipBottomBarModel
(rather than a tuple, which makes for terrible error messages in VSCode (the LSP specifically)).
Then I add a fromStreams()
method to this WorshipBottomBarModel
, that accepts 7 streams and returns a Stream<WorshipBottomBarModel>
.
The result looks like this:
StreamBuilder(
stream: WorshipBottomBarModel.fromStreams(
contents: contents$,
running: running,
playing: audio.playingStream,
soloPlaying: soloAudio.playingStream,
editing: editing,
soloing: soloing$,
showCountdown: showCountdown,
),
initialData: WorshipBottomBarModel(
contents: <WorshipContent>[],
running: false,
playing: false,
soloPlaying: false,
editing: false,
soloing: null,
showCountdown: false,
),
builder: (context, snapshot) {
var data = snapshot.requireData;
if (data.contents.isEmpty || data.editing || data.showCountdown) {
return const SizedBox.shrink();
}
// ... lots of code
},
),
With this change, I can F2
rename the data on this model to better reflect their purpose or reason. In addition, I could add a default
factory method that constructs the initialData
for the StreamBuilder
.
StreamBuilder(
stream: WorshipBottomBarModel.fromStreams(
contents: contents$,
isWorshipRunning: running,
isWorshipAudioPlaying: audio.playingStream,
isSoloAudioPlaying: soloAudio.playingStream,
isEditing: editing,
isSoloing: soloing$,
hideBar: showCountdown,
),
initialData: WorshipBottomBarModel.defaultData(),
builder: (context, snapshot) {
var data = snapshot.requireData;
if (data.contents.isEmpty || data.editing || data.showCountdown) {
return const SizedBox.shrink();
}
// ... lots of code
},
),
Going back to the Solo Button combiner, here’s what theses changes look like (note we’re using my custom StreamToWidget
)
StreamToWidget(
SoloDataModel.fromStreams(
isWorshipRunning: running,
soloedContent: soloing$,
worshipAudioPlaying: audio.playingStream,
soloAudioPlaying: soloAudio.playingStream,
),
converter: (data) {
bool isSoloed = data.soloedContentIndex == contentIndex;
bool hasDedicatedSoloTrack =
soloMap?.containsKey(content.id) == true;
bool isSoloPaused = isSoloed &&
(hasDedicatedSoloTrack
? !data.soloAudioPlaying
: !data.worshipAudioPlaying);
if (data.isWorshipRunning &&
data.soloedContentIndex == null) {
return const SizedBox();
}
return FloatingActionButton.small(
// .. lots of code
);
},
)
Your ideas?
I’ll continue what I can to bring new features into the app, and writing down any progress that I make.
If anyone else find themselves in this situation, how do you deal with it, please write your ideas down below.
In the meantime, if you find yourself struggling with difficult problems with RxDart (or even RxJS!), you can comment them down below, or email me at bookofcooks123@gmail.com
.
I’ve dealt with this technology and reactive state management for more than half a decade, so I’m willing to help (no charge).
You can also ask on the FlutterDev
and The Programmer's Hangout
help channels. I’m active there, along with hundreds of people willing to help.
This content originally appeared on DEV Community and was authored by BookOfCooks