Cracking the Coding Interview in Flutter: Design the Call Center
I absolutely love this book: Cracking Coding Interview by Gayle Laakmann McDowell.
I decided to dive deeper into Dart. It was interesting for me to reimplement some of the tasks in the book but this time in Flutter+Dart.
We will solve following question (quote from the book):
Imagine you have a call center with three levels of employees: respondent, manager, and director. An incoming telephone call must be first allocated to a respondent who is free. If the respondent can’t handle the call, he or she must escalate the call to a manager. If the manager is not free or not able to handle it, then the call should be escalated to a director. Design the classes and data structures for this problem. Implement a method dispatchCall() which assigns a call to the first available employee.
Github with working app: https://github.com/gladimdim/flutter_callcenter
Solution
We can break the solution into smaller parts. One part is responsible for ‘backend’ tasks like handling calls, dispatching them to the correct responders. The other is a UI part. It will reuse methods to display current status of CallCenter on a mobile device.
The backend solution can be broken into more atomic parts: CallCenter is a main entry point for UI. CallCenter needs to know how to work with Responders. Responders must know how to work with incoming Calls.
One of the solutions is to break CallCenter into following classes
CallCenter implementation
- Responder Class
- Call Class
- CallCenter Class
CallCenter Implementation
Call Class
According to the description we have to implement CallCenter class with the dispatchCall method. An input for this method will be an instance of another class Call
.
class Call {
String msg;
Call({this.msg});
}
It accepts string with a question which responder must answer. Let us also add API to end the call. It would be nice to notify possible listeners that the call has ended. For this we will add such methods: addEndCallback
and endCall
:
class Call {
String msg;
List<Function> endCallCallbacks = [];
Call(this.msg);
addEndCallback(Function callback) {
endCallCallbacks.add(callback);
}
endCall() {
endCallCallbacks.forEach((f) => f());
}
}
addEndCallback
pushes new callback functions to the internal list. Once the endCall
is called, we call each registered callback. In this way all parties which wanted to be notified about end of the call will have a chance to react.
Responder Class
Now we need to implement a Responder class. According to the task there are three possible responder types:
**enum** ResponderType { Manager, Director, Worker }
Responder class has such methods:
respondToCall(Call)
accepts the incoming call.isBusy
returns true if Responder is already answering a call.endCurrentCall()
ends current call. After this method is called Responder is available again.- If responder is busy and we try to assign another call to it, then it will throw exception:
**class** ResponderBusyException **implements** Exception {
String msg;
ResponderBusyException(**this**.msg);
}
Full listing for Responder Class:
Notice line #21 we save current call to temporary variable, then nullify pointer call
and only then call endCall
method. This is done to avoid issues when callback handlers called inside endCall
method try to read status of our Responder. They will see that Responder is still busy! (call is not null).
CallCenter Class
The main class should hold a list of all responders: workers, managers and directors:
List<Responder> workers = [];
List<Responder> managers = [];
List<Responder> directors = [];
For convenience we will provide auxiliary list of lists:
List<List<Responder>> allProcessors = [];
allProcessors[0]
will point to workers, allProcessors[1]
to managers and allProcessors[2]
to directors. This additional list will help us to iterate over all available responders.
Now let us implement a constructor. It will generate list of workers, managers and directors and assign them into appropriate lists:
CallCenter() {
workers.addAll([
Responder(type: ResponderType.Worker, name: "Responder A"),
Responder(type: ResponderType.Worker, name: "Responder B"),
Responder(type: ResponderType.Worker, name: "Responder C"),
Responder(type: ResponderType.Worker, name: "Responder D"),
]);
managers.addAll([
Responder(type: ResponderType.Manager, name: "Manager A"),
Responder(type: ResponderType.Manager, name: "Manager B"),
Responder(type: ResponderType.Manager, name: "Manager C"),
]);
directors.addAll([
Responder(type: ResponderType.Director, name: "Director A"),
Responder(type: ResponderType.Director, name: "Director B"),
]);
allProcessors.add(workers);
allProcessors.add(managers);
allProcessors.add(directors);
}
Add Queue of Incoming Calls
If all responders are busy then we have to add incoming Call to the queue. For this we use List<Call>
data structure:
**class** CallCenter {
List<Responder> workers = [];
List<Responder> managers = [];
List<Responder> directors = [];
List<List<Responder>> allProcessors = [];
**List<Call> queueCalls = [];**
Implement dispatchCall
According to the task, the Call must be first processed by Worker. If all of them are busy we redirect it to Manager and only then to Director. If all of them are busy we add incoming call to the Queue.
dispatchCall(Call call) {
Responder processor;
for (var i = 0; i < allProcessors.length; i++) {
try {
processor = allProcessors[i].firstWhere((Responder p) => !p.isBusy());
} catch (e) {
print('Switching to next level');
continue;
}
if (processor != null) {
break;
}
}
// if all processors are busy we add call to queue
if (processor == null) {
queueCalls.add(call);
} else {
processor.respondToCall(call);
// we add a callback listener to now when the call is ended to
// check the queue and do other stuff in CallCenter
call.addEndCallback(callEnded);
}
}
callEnded
handler will check if queue has incoming calls and dispatch them:
callEnded() {
if (queueCalls.length > 0) {
dispatchCall(queueCalls[0]);
queueCalls.removeAt(0);
}
}
Backend Summary
We implemented the main task: CallCenter
can accept incoming calls and dispatch them to Workers. If they are busy then it redirects to Managers and then to Directors. We can use Call.endCall
to end call it our CallCenter
listener can catch this event and process it in callEnded
method. If all responders are busy we add Call
to Queue. callEnded
handles case when some responder is freed and it can immediately respond to the enqueued call.
We will improve our API to satisfy UI needs in next paragraphs.
Flutter UI Part
Our UI must know as little as possible about CallCenter implementation. It will be used to render list of responders and queue. User can also end specific call.
<noscript><img alt="" class="fm n o fl x" src="miro.medium.com/max/1656/1*HyneXGMLMZPN0w1L.." width="828" height="1792" role="presentation"/></noscript>
All Responders are available!
Widget Structure
There is huge amount of high quality articles on how to build UI in Flutter, so I will just outline the basic structure of our UI.
Main application is a Column. Column’s children are: List of Rows of responders (workers, managers, directors), and a row of buttons. Then goes the Queue.
Responders section is a ListView (with a horizontal scroll when there are a lot of responders):
Widget _buildResponderRow(List<Responder> group) {
return Container(
height: 100.0,
child: ListView.builder(
itemCount: group.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return _buildResponderView(group[index]);
},
),
);
}
Row of Responders is a Column with Responder Name and button to end call:
Widget _buildResponderView(Responder responder) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
responder.name,
style: TextStyle(
color: responder.isBusy() ? Colors.red : Colors.green,
fontWeight: FontWeight.bold
),
),
IconButton(
icon: Icon(Icons.call_end),
onPressed: responder.isBusy() ? responder.endCurrentCall : null,
)
]),
);
}
Row of buttons:
Widget _buildButton() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: RaisedButton.icon(
icon: Icon(Icons.call),
onPressed: addCall,
label: Text("Add Call"),
),
),
RaisedButton.icon(
icon: Icon(Icons.call_made),
onPressed: endCall,
label: Text("End Random Call"),
)
],
);
}
Widget _buildQueueView(List<Call> queueCalls) {
return Column(
// mainAxisAlignment: MainAxisAlignment.center,
children: queueCalls.map((call) => Text(call.msg)).toList());
}
Linking UI to CallCenter backend
So we outlined the UI structure. But how can we get list of responders and how can we rebuild our UI when their statuses change? We could listen in our UI to Calls
via endCallCallback
, but that would break our abstraction. UI has to know as little as possible about CallCenter internals.
StreamBuilder comes to the rescue!
We can implement stream of change events in CallCenter
and our UI can listen to them. We then just rebuild our UI with latest data. With this approach we do not care WHAT actually changed. We care only about the fact that SOMETHING changed and we must rebuild the UI.
We enhance our CallCenter
implementation with three additional stream: Responder Stream, Queue Stream and merged Responder+Queue stream.
StreamController<List<List<Responder>>> _responderStream = StreamController();
StreamController<List<Call>> _queue = StreamController();
Stream changes;
// in constructor:
changes = StreamGroup._merge_([_responderStream.stream, _queue.stream]);
Stream changes
is used by our UI to get notified that something changed and Flutter must rebuild the UI.
Our main build
method now looks like this:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Call Center'),
),
body: StreamBuilder(
stream: callCenter.changes,
builder: (context, snapshot) {
return Column(
children: _buildBody() +
[_buildButton()] +
[_buildQueueView(callCenter.queueCalls)],
);
}),
bottomNavigationBar: BottomAppBar(
child: Container(
height: 50.0,
),
),
);
}
Body will be rebuilt each time some value is put into callCenter.changes
stream. As we saved a pointer to callCenter
we do not actually care on the snapshot.data
variable, but we could reuse it.
Now let us implement backend part of our streams.
In dispatchCall
method we push new values to our streams:
**if** (processor == **null**) {
queueCalls.add(call);
**_queue.sink.add(queueCalls);**
} **else** {
processor.respondToCall(call);
call.addEndCallback(callEnded);
** _responderStream.sink.add(allProcessors);**
}
callEnded
method:
callEnded() {
_responderStream.sink.add(allProcessors);
**if** (queueCalls.length > 0) {
dispatchCall(queueCalls[0]);
queueCalls.removeAt(0); **_queue.sink.add(queueCalls);**
}
}
Now each time a new Call
is put in queue or is answered by some Responder
our StreamBuilder will rebuild our UI!
Summary
We implemented CallCenter
design by using Dart Classes. There are a lot of different variations of solutions for this task. This is just one of them. At first we created ‘algorithmic’ part. Then we created UI. To connect our UI to the backend we used StreamBuilder
to get notifications when something has changed in CallCenter
. You can see that our UI relies only on several API calls: isBusy
, allProcessers
, endCall
. The less our UI knows about internal parts of CallCenter
the better. We can change implementation of CallCenter
methods without touching the UI at all.
Full project is available at https://github.com/gladimdim/flutter_callcenter