Meeting Room L̶i̶t̶t̶l̶e̶ Helper v 2
This article describes in detail the development stages of the Meeting Room Helper mobile application: from the inception of the idea to the release. The application is written in Kotlin and built on a simplified MVVM architecture, without using data binding. The UI part is updated using LiveData objects. The reasons for refusing data binding are detailed and explained. The architecture uses a number of interesting solutions that make it possible to logically split the program into small files, which ultimately simplifies code support.
3 years ago, our company came up with the idea to develop a small project for instant booking of meeting rooms. Most HR managers and Arcadia prefer to use the Outlook calendar for such purposes, but what about the rest?
I will give 2 examples from the life of the developer
That is why 2.5 years ago each of the meeting rooms was equipped with its own tablet:
For this project, my colleague developed the first version of the application: Meeting Room Little Helper ( here you can read about it ). MRLH allowed to book a reservation, cancel and renew a reservation, showed the status of the remaining conversations. Recognizing an employee’s identity (using the Microsoft Face API cloud service and our internal analyzers) has become an innovative “trick”. The application turned out to be solid and served the company faithfully for 2.5 years.
But time passed ... New ideas appeared. I wanted something fresh, and so we decided to rewrite the application.
As often happens - but, unfortunately, not always - the development began with the preparation of technical specifications. First of all, we called the guys who most often use tablets for reservations. It just so happened that most of all they were addicted to HRs and managers who had previously used Outlook exclusively. From them we received the following feedback (from the requirements it’s immediately clear what HR asked for and what managers asked for):
Everything is clear with the customer’s wishes, but what about the technical requirements and the future? Add a few requirements for the project from the developers guild:
Total 8 points. The requirements are fairly fair. Additionally, we stipulate the general development rules:
A start. It, as always, is enthusiastic! Let's see what will happen next.
UX Design Application Design:
This is the main screen. It will be displayed most of the time. All the necessary information is ergonomically located here:
Please note: the dial only displays 12 hours, as the system is configured according to the needs of the company (Arcadia tablets work from 8 am to 8 pm, turn on and off automatically)
To reserve a room, just call the booking window and indicate the duration of the rally. The steps for booking the remaining rooms are similar, they only begin by clicking on the room icon.
If you want to schedule a meeting for a specific time, then go to the next tab, to the list of meetings that will take place today in the meeting room, and click on free time. Further, everything is as in the first case.
A complete transition tree should look something like this:
Let's try to implement this competently.
Development techniques are developing rather quickly and changing. For another 2 years, Java was the official Android development language. Everyone wrote in Java and used data binding. Now, it seems to me, we are moving towards reactive programming and Kotlin. Java is a great language, but it has some imperfections compared to what Kotlin and AndroidX have to offer. Kotlin and AndroidX can reduce the use of data binding to a minimum, if not completely exclude it. Below I will try to explain my point of view.
I think many Android developers have already switched to Kotlin, and therefore agree with me that writing a new Android project in 2019 in any language other than Kotlin is like fighting the sea. Of course you can argue, but what about Flutter and Dart? What about C ++, C #, and even Cordova? To which I will answer: the choice is always yours.
In 480 BC the Persian king Xerxes ordered his soldiers to cross the sea as a punishment for destroying part of his army during a storm, and five centuries later, the Roman emperor Caligula declared war on Poseidon. A matter of taste. For 9 out of 10, Kotlin is good, but for 10 it may be bad. It all depends on you, on your desires and aspirations.
Kotlin is my choice. The language is simple and beautiful. Writing on it is easy and pleasant, and most importantly, there is no need to write too much: data class, object, optional setter and getter, simple lambda expressions and extension functions. This is just a tiny part of what this language has to offer. If you have not switched to Kotlin yet - feel free to go! In the section with practice, I will demonstrate some of the advantages of the language (it is not an advertising offer).
MVVM is currently the recommended application architecture from Google. During development, we will adhere to this particular pattern, however, we will not fully observe it, since MVVM recommends using data binding, but we refuse it.
Pros of MVVM
Why is MVVM dangerous?
Rules for working with MVVM
Let's start with the most blunders and go to the less blunders:
From the definition, you can make a simple conclusion: LiveData is a reliable reactive programming tool. We will use it to update the UI part without data binding. Why is that?
The structure of XML files does not allow a concise distribution of data obtained from.... If everything is clear with small files, then what about large files? What to do with complex screens, multiple include and passing multiple fields? Use models everywhere? Get stiff field bindings? And if the field should be formatted, call methods from Java packages? This makes the code hopelessly and completely spaghetti. Not at all what MVVM promised.
Rejecting data binding will make changes to the UI part transparent. All updates will occur directly inside the observer. Because Since the Kolin code is concise and clear, we won’t get problems with bloated observer. Writing and maintaining code will become easier. XML files will be used only for design - no property inside.
Data binding is a powerful tool. It is great for solving some problems, and it harmonizes well with Java, but with Kotlin ... With Kotlin, in most cases, data binding is just rudimentary. Data binding only complicates the code and does not give any competitive advantages.
In Java, you had a choice: either use data binding, or write a lot of ugly code. In Kotlin, you can access view elements directly, bypassing findViewById (), as well as its property. For instance:
A logical question arises: why bother with gardening models inside XML files, call Java methods in XML files, overload the logic of the XML part if all this can be avoided?
Coroutines are incredibly lightweight and easy to use. They are ideal for most simple asynchronous tasks: processing query results, updating UI, etc.
Coroutines can effectively replace Thread () and Rx-Java in cases where high performance is not required, because they pay for lightness with speed. Rx-Java, undoubtedly, is more functional, however for simple tasks all its assets are not required.
To work with Outlook services, the Microsoft Graph API will be used. With the appropriate permissions, through it you can get all the necessary information about employees, rooms and event-ahs (meetings). For face recognition, the Microsoft Face API cloud service will be used.
Looking a little ahead, I will say that to solve the scalability problem, Firebase cloud storage was used. This will be discussed below.
It is quite difficult to make the system fully or partially scalable. This is especially difficult to do if the first version of the application was not scalable, and the second should become. Application v1 sent requests to all rooms at once. Each of the tablets regularly sent requests to the server to update all the data. At the same time, the devices did not synchronize with each other, because the project simply does not have its own server.
Of course, if we go along the same path and send N requests from each of the N tablets, then at some point we will either overturn the Microsoft Graph API or get our system freezing.
It would be logical to use a client-server solution in which the server polls the graph, accumulates data and, upon request, provides information to the tablets, but here we are met by reality. The project team consists of 2 people (Android developer and designer). They need to meet the deadline of 7 weeks and the backend is not provided, because scaling is a requirement from the developer. But this does not mean that the idea must be abandoned?
Probably the only right solution in this situation will be the use of cloud storage. Firebase will replace the server and act as a buffer. Then it turns out the following: each tablet polls only its address from the Microsoft Graph API, and, if necessary, synchronizes data in the cloud storage, from where it can be read by other devices.
The advantage of this implementation will be a quick response, because Firebase works in real-time mode. We will reduce the number of requests sent to the server N times, which means the device will work on battery a little longer. From a financial point of view, the project did not rise in price, because For this project, the free version of Firebase is enough with multiple reserves: 1 GB of storage, 10 thousand authorizations per month and 100 connections at a time. The disadvantages could include dependence on a third-party framework, but Firebase inspires confidence in us, because It is a stable product maintained and developed by Google.
The general idea of the new system was as follows: N tablets and a cloud platform for real-time data synchronization. Let's start designing the application itself.
It would seem that I recently established the rules of good form and immediately violate one of them. Unlike the recommended use of LiveData inside the ViewModel, in this project LiveData objects are initialized in the repository, and all repositories are declared as singleton. Why is that?
A similar solution is associated with the application mode. Tablets are open from 8am to 8pm. All this time, only the Meeting Room Helper has been launched on them. As a result, many objects can and should be long-lived (that is why all repositories are designed as singleton).
In the course of work, UI content is regularly switched, which in turn entails the creation and recreation of ViewModel objects. It turns out that if you use LiveData inside the ViewModel, then for each created fragment its own ViewModel will be created with a set of specified LiveData objects. If 2 similar fragments are displayed simultaneously on the screen, with different ViewModel and a common Base-ViewModel, then during initialization there will be a duplication of LiveData objects from the Base-ViewModel. In the future, these duplicates will take up memory space until they are destroyed by the "garbage collector." Because If we already have a repository in the form of a singleton and we want to minimize the cost of re-creating screens, it would be wise to transfer LiveData objects to a singleton-repository, thereby facilitating ViewModel objects and speeding up the application.
Of course, this does not mean that you need to transfer all LiveData from the ViewModel to the repository, but you should more thoughtfully approach this issue and make your choice consciously. The disadvantage of this approach is the increase in the number of long-lived objects, because all repositories are defined as singleton and each of them stores LiveData objects. But in a specific case, Meeting Room Helper is not a minus, because the application runs non-stop all day, without switching context to other applications.
Background logic has been moved to Intent-Service:
The result was a rather flexible and correct architecture from the point of view of the distribution of responsibilities, which meets all the requirements of modern development. If in the future we abandon the Microsoft Graph API, Firebase, or any other module, they can easily be replaced with new ones without interfering with the rest of the application. The presence of an extensive system of “presenters” made it possible to take all the data processing functions beyond the core. As a result, the architecture has become crystal clear, which is a big plus. The problem of an overgrown ViewModel has completely disappeared.
Below I will give an example of the commonly used bundle in a developed application.
Depending on the state of the meeting room, the dial shows one of the following states:
In addition, there are temporary arches of rallies along the dial outline, and the center counts down until the end of the meeting or until the next rally begins. All this is done by the canvas library we developed. If the grid of meetings has changed, we must update the data in the library.
Since LiveData is announced in Repositories, it’s most logical to start with them.
FirebaseRoomRepository - a class responsible for sending and processing requests in Firebase related to the Room model.
To demonstrate, the listener firebase initialization code was slightly simplified (the reconnect function was removed). Let's take a look at the points of what is happening here:
The functions themselves are moved to a separate FirebaseRoomRepositoryPresenter file and decorated as extension functions.
An example of the extension function from FirebaseRoomRepositoryPresenter
Also for a general understanding of the picture I will give a listing of the Room object.
All Repositories classes are hidden behind the facade class.
Such an organization allows you to comfortably fit from 20 to 30 requests in one root repository. If your application has more requests, you will have to divide the root facade into 2 or more.
BaseViewModel is the base ViewModel from which all ViewModels are inherited. It includes one single currentRoom object, used universally.
In order to convert data from one format to another, we will use transformation. To do this, create a MainFragmentViewModel and inherit it from BaseViewModel .
MainFragmentViewModel is a derived class from BaseViewModel. This ViewModel is used only in MainFragment.
The first option is used to convert data from one type to another, which is what we needed, and the second option is needed to execute some business logic. However, data conversion does not occur. Remember that android import in ViewModel is not valid. Therefore, I start additional requests from here or restart services as necessary.
Important notice! In order for the transformation or mediator to work, someone must be subscribed to them from fragment or activity. Otherwise, the code will not be executed, because no one will expect a result (these are observer objects).
The final step in converting data to result. MainFragment includes a dial library and a View-Pager at the bottom of the screen.
Thus, I want to demonstrate the simplicity and beauty of MVVM and LiveData without using data binding. Please note that in this project I violate the generally accepted rule by placing LiveData in the Repository due to the specifics of the project. However, if we moved them to the ViewModel, the overall picture would remain unchanged.
As a cherry on a cake, I have prepared for you a short video with a demonstration (names are smeared in accordance with security requirements, I apologize):
As a result of the application in the first month, some bugs were revealed in the display of cross rallies (Outlook allows you to create several events at the same time, while our system does not). Now the system has been working for 3 months. Errors or failures are not observed.
PS Thanks jericho_code for the comment. In Kotlin, you can and should initialize List <> in the model using emptyList (), then an extra object is not created.
Project Description
3 years ago, our company came up with the idea to develop a small project for instant booking of meeting rooms. Most HR managers and Arcadia prefer to use the Outlook calendar for such purposes, but what about the rest?
I will give 2 examples from the life of the developer
- Any team periodically has a spontaneous desire to hold a quick rally for 5-10 minutes. This desire can overtake developers in any corner of the office, and so as not to distract colleagues around them, they (developers and not only) begin to look for a free conversation. Migrating from room to room (in our office the meeting rooms are arranged in a row), colleagues “carefully check” which of the rooms is currently free. As a result, they distract colleagues inside. Such guys have always been and always will be, even if execution is to be shot in the corporate charter for the interruption of the rally. Who understood, he will understand.
- And here is another case. You have just left the dining room and are heading to yourself, but here your colleague (or manager) from another department intercepts you. He wants to tell you something urgent, and for these purposes you need a meeting room. According to the regulations, you must first book a room (from your phone or computer) and only then occupy it. It’s good if you have a mobile phone with mobile Outlook. And if not? Go back to the computer, then again to return to the meeting room? To force each employee to put Outlook Express on the phone and make sure that everyone carries the phones with them? These are not our methods.
That is why 2.5 years ago each of the meeting rooms was equipped with its own tablet:
For this project, my colleague developed the first version of the application: Meeting Room Little Helper ( here you can read about it ). MRLH allowed to book a reservation, cancel and renew a reservation, showed the status of the remaining conversations. Recognizing an employee’s identity (using the Microsoft Face API cloud service and our internal analyzers) has become an innovative “trick”. The application turned out to be solid and served the company faithfully for 2.5 years.
But time passed ... New ideas appeared. I wanted something fresh, and so we decided to rewrite the application.
Technical task
As often happens - but, unfortunately, not always - the development began with the preparation of technical specifications. First of all, we called the guys who most often use tablets for reservations. It just so happened that most of all they were addicted to HRs and managers who had previously used Outlook exclusively. From them we received the following feedback (from the requirements it’s immediately clear what HR asked for and what managers asked for):
- you must add the ability to book any meeting room from any tablet (previously, each tablet allowed you to book only your room);
- it would be cool to look at the schedule of rallies for an all-day meeting (ideally, for any day);
- the entire development cycle must be carried out in a short time (for 6-7 weeks).
Everything is clear with the customer’s wishes, but what about the technical requirements and the future? Add a few requirements for the project from the developers guild:
- The system should work both with existing tablets, and with new ones;
- scalability of the system - from 50 conversations and above (this should be enough with a margin for most customers if the system starts to replicate);
- maintaining the previous functionality (the first version of the application used the Java API to communicate with Outlook services, and we planned to replace it with a specialized Microsoft Graph API, so it was important not to lose functionality);
- minimization of energy consumption (tablets are powered by an external battery, because the business center did not allow drilling its walls to lay our wires);
- new UX / UI design, ergonomically reflecting all the innovations.
Total 8 points. The requirements are fairly fair. Additionally, we stipulate the general development rules:
- use only advanced technologies (this will allow the team to develop as specialists and not stagnate in one place, while simplifying project support in the foreseeable future);
- follow best practices, but do not blindly take them for granted, as the main rule of any professional (and a developer striving for this) is to evaluate everything critically;
- Writing clean and tidy code (perhaps this is the most difficult when you are trying to combine innovation and tight development time).
A start. It, as always, is enthusiastic! Let's see what will happen next.
Design
UX Design Application Design:
This is the main screen. It will be displayed most of the time. All the necessary information is ergonomically located here:
- the name of the room and its number;
- current status;
- time until the next meeting (or until its end);
- the statuses of the remaining rooms at the bottom of the screen.
Please note: the dial only displays 12 hours, as the system is configured according to the needs of the company (Arcadia tablets work from 8 am to 8 pm, turn on and off automatically)
To reserve a room, just call the booking window and indicate the duration of the rally. The steps for booking the remaining rooms are similar, they only begin by clicking on the room icon.
If you want to schedule a meeting for a specific time, then go to the next tab, to the list of meetings that will take place today in the meeting room, and click on free time. Further, everything is as in the first case.
A complete transition tree should look something like this:
Let's try to implement this competently.
Technology stack
Development techniques are developing rather quickly and changing. For another 2 years, Java was the official Android development language. Everyone wrote in Java and used data binding. Now, it seems to me, we are moving towards reactive programming and Kotlin. Java is a great language, but it has some imperfections compared to what Kotlin and AndroidX have to offer. Kotlin and AndroidX can reduce the use of data binding to a minimum, if not completely exclude it. Below I will try to explain my point of view.
Kotlin
I think many Android developers have already switched to Kotlin, and therefore agree with me that writing a new Android project in 2019 in any language other than Kotlin is like fighting the sea. Of course you can argue, but what about Flutter and Dart? What about C ++, C #, and even Cordova? To which I will answer: the choice is always yours.
In 480 BC the Persian king Xerxes ordered his soldiers to cross the sea as a punishment for destroying part of his army during a storm, and five centuries later, the Roman emperor Caligula declared war on Poseidon. A matter of taste. For 9 out of 10, Kotlin is good, but for 10 it may be bad. It all depends on you, on your desires and aspirations.
Kotlin is my choice. The language is simple and beautiful. Writing on it is easy and pleasant, and most importantly, there is no need to write too much: data class, object, optional setter and getter, simple lambda expressions and extension functions. This is just a tiny part of what this language has to offer. If you have not switched to Kotlin yet - feel free to go! In the section with practice, I will demonstrate some of the advantages of the language (it is not an advertising offer).
Model-View-ViewModel
MVVM is currently the recommended application architecture from Google. During development, we will adhere to this particular pattern, however, we will not fully observe it, since MVVM recommends using data binding, but we refuse it.
Pros of MVVM
- Differentiation of business logic and UI. In the correct implementation of MVVM, there should not be a single import android in the ViewModel, except for LiveData objects from AndroidX or Jetpack packages. Proper use automatically leaves all UI work inside fragments and activities. Isn't that great?
- The level of encapsulation is pumped. It will be easier to work as a team: now you can work all together on one screen and not interfere with each other. While one developer works with the screen, another can build a ViewModel, and a third can write queries in the Repository.
- MVVM has a positive effect on writing unit tests. This item follows from the previous one. If all classes and methods are encapsulated from working with the UI, they can easily be tested.
- A natural solution with screen rotation. No matter how strange it may sound, but this feature is acquired automatically, with the transition to MVVM (because the data is stored in the ViewModel). If you check quite popular applications (VK, Telegram, Sberbank-Online and Aviasales), it turns out that exactly half of them are not able to rotate the screen. Which causes me some surprise and misunderstanding as a user of these applications.
Why is MVVM dangerous?
- Memory leak. This dangerous error happens if you break the laws of using LiveData and observer. We will examine this error in detail in the practice section.
- Sprawling ViewModel. If you try to fit all the business logic into the ViewModel, you will get an unreadable code. The way out of this situation may be splitting the ViewModel into a hierarchy, or using Presenters. That is exactly what I did.
Rules for working with MVVM
Let's start with the most blunders and go to the less blunders:
- request body should not be in ViewModel (only in Repository);
- LiveData objects are defined in the ViewModel, they do not throw themselves inside the Repository, because requests in the Repository are processed using Rx-Java (or coroutines);
- all processing functions should be moved to third-party classes and files ("Presenters"), so as not to clutter the ViewModel and not distract from the essence.
Livedata
LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.Source: developer.android.com/topic/libraries/architecture/livedata
From the definition, you can make a simple conclusion: LiveData is a reliable reactive programming tool. We will use it to update the UI part without data binding. Why is that?
The structure of XML files does not allow a concise distribution of data obtained from.... If everything is clear with small files, then what about large files? What to do with complex screens, multiple include and passing multiple fields? Use models everywhere? Get stiff field bindings? And if the field should be formatted, call methods from Java packages? This makes the code hopelessly and completely spaghetti. Not at all what MVVM promised.
Rejecting data binding will make changes to the UI part transparent. All updates will occur directly inside the observer. Because Since the Kolin code is concise and clear, we won’t get problems with bloated observer. Writing and maintaining code will become easier. XML files will be used only for design - no property inside.
Data binding is a powerful tool. It is great for solving some problems, and it harmonizes well with Java, but with Kotlin ... With Kotlin, in most cases, data binding is just rudimentary. Data binding only complicates the code and does not give any competitive advantages.
In Java, you had a choice: either use data binding, or write a lot of ugly code. In Kotlin, you can access view elements directly, bypassing findViewById (), as well as its property. For instance:
// Instead of TextView textView = findViewById(R.id.textView)
textView.text = "Hello, world!"
textView.visibility = View.VISIBLE
A logical question arises: why bother with gardening models inside XML files, call Java methods in XML files, overload the logic of the XML part if all this can be avoided?
Coroutines instead of Thread () and Rx-Java
Coroutines are incredibly lightweight and easy to use. They are ideal for most simple asynchronous tasks: processing query results, updating UI, etc.
Coroutines can effectively replace Thread () and Rx-Java in cases where high performance is not required, because they pay for lightness with speed. Rx-Java, undoubtedly, is more functional, however for simple tasks all its assets are not required.
Microsoft and the rest
To work with Outlook services, the Microsoft Graph API will be used. With the appropriate permissions, through it you can get all the necessary information about employees, rooms and event-ahs (meetings). For face recognition, the Microsoft Face API cloud service will be used.
Looking a little ahead, I will say that to solve the scalability problem, Firebase cloud storage was used. This will be discussed below.
Architecture
Scalability issues
It is quite difficult to make the system fully or partially scalable. This is especially difficult to do if the first version of the application was not scalable, and the second should become. Application v1 sent requests to all rooms at once. Each of the tablets regularly sent requests to the server to update all the data. At the same time, the devices did not synchronize with each other, because the project simply does not have its own server.
Of course, if we go along the same path and send N requests from each of the N tablets, then at some point we will either overturn the Microsoft Graph API or get our system freezing.
It would be logical to use a client-server solution in which the server polls the graph, accumulates data and, upon request, provides information to the tablets, but here we are met by reality. The project team consists of 2 people (Android developer and designer). They need to meet the deadline of 7 weeks and the backend is not provided, because scaling is a requirement from the developer. But this does not mean that the idea must be abandoned?
Probably the only right solution in this situation will be the use of cloud storage. Firebase will replace the server and act as a buffer. Then it turns out the following: each tablet polls only its address from the Microsoft Graph API, and, if necessary, synchronizes data in the cloud storage, from where it can be read by other devices.
The advantage of this implementation will be a quick response, because Firebase works in real-time mode. We will reduce the number of requests sent to the server N times, which means the device will work on battery a little longer. From a financial point of view, the project did not rise in price, because For this project, the free version of Firebase is enough with multiple reserves: 1 GB of storage, 10 thousand authorizations per month and 100 connections at a time. The disadvantages could include dependence on a third-party framework, but Firebase inspires confidence in us, because It is a stable product maintained and developed by Google.
The general idea of the new system was as follows: N tablets and a cloud platform for real-time data synchronization. Let's start designing the application itself.
LiveData in Repository
It would seem that I recently established the rules of good form and immediately violate one of them. Unlike the recommended use of LiveData inside the ViewModel, in this project LiveData objects are initialized in the repository, and all repositories are declared as singleton. Why is that?
A similar solution is associated with the application mode. Tablets are open from 8am to 8pm. All this time, only the Meeting Room Helper has been launched on them. As a result, many objects can and should be long-lived (that is why all repositories are designed as singleton).
In the course of work, UI content is regularly switched, which in turn entails the creation and recreation of ViewModel objects. It turns out that if you use LiveData inside the ViewModel, then for each created fragment its own ViewModel will be created with a set of specified LiveData objects. If 2 similar fragments are displayed simultaneously on the screen, with different ViewModel and a common Base-ViewModel, then during initialization there will be a duplication of LiveData objects from the Base-ViewModel. In the future, these duplicates will take up memory space until they are destroyed by the "garbage collector." Because If we already have a repository in the form of a singleton and we want to minimize the cost of re-creating screens, it would be wise to transfer LiveData objects to a singleton-repository, thereby facilitating ViewModel objects and speeding up the application.
Of course, this does not mean that you need to transfer all LiveData from the ViewModel to the repository, but you should more thoughtfully approach this issue and make your choice consciously. The disadvantage of this approach is the increase in the number of long-lived objects, because all repositories are defined as singleton and each of them stores LiveData objects. But in a specific case, Meeting Room Helper is not a minus, because the application runs non-stop all day, without switching context to other applications.
Resulting architecture
- All requests are executed in repositories. All repositories (in Meeting Room Helper there are 11 of them) are designed as singleton. They are divided by type of returned objects and hidden behind the facades.
- Business logic resides in the ViewModel. Thanks to the use of "Presenters", the total size of all ViewModel (there are 6 in the project) turned out to be less than 120 lines.
- Activity and fragment are only involved in changing the UI part, using observer and LiveData returned from the ViewModel.
- Functions for processing and generating data are stored in "presenter". Actively used permission functions from Kotlin for data processing.
Background logic has been moved to Intent-Service:
- Event-Update-Service. Service responsible for synchronizing the data of the current room in Firebase and Graph API.
- User-Recognize-Service. Runs only on the master tablet. Responsible for adding new staff to the system. Checks a list of already trained persons with a list from Active Directory. If new people appear, the service adds them to the Face API and retrains the neural network. Upon completion of the operation, it is turned off. It starts when the application starts.
- Online-Notification-Service notifies other tablets that this tablet is functioning, i.e. The external battery is not exhausted. It works through Firebase.
The result was a rather flexible and correct architecture from the point of view of the distribution of responsibilities, which meets all the requirements of modern development. If in the future we abandon the Microsoft Graph API, Firebase, or any other module, they can easily be replaced with new ones without interfering with the rest of the application. The presence of an extensive system of “presenters” made it possible to take all the data processing functions beyond the core. As a result, the architecture has become crystal clear, which is a big plus. The problem of an overgrown ViewModel has completely disappeared.
Below I will give an example of the commonly used bundle in a developed application.
Practice. Watch Updates
Depending on the state of the meeting room, the dial shows one of the following states:
In addition, there are temporary arches of rallies along the dial outline, and the center counts down until the end of the meeting or until the next rally begins. All this is done by the canvas library we developed. If the grid of meetings has changed, we must update the data in the library.
Since LiveData is announced in Repositories, it’s most logical to start with them.
Repositories
FirebaseRoomRepository - a class responsible for sending and processing requests in Firebase related to the Room model.
// 1.
object FirebaseRoomRepository {
private val database = FirebaseFactory.database
val rooms: MutableList = ArrayList()
// 2.
var currentRoom: MutableLiveData = MutableLiveData()
val onlineStatus: MediatorLiveData> = MediatorLiveData()
var otherRooms: MutableLiveData> = MutableLiveData()
var ownRoom: MutableLiveData = MutableLiveData()
// 3.
private val roomsListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
updateRooms(dataSnapshot)
}
override fun onCancelled(error: DatabaseError) {}
}
init {
// 4.
database.getReference(ROOMS_CURRENT_STATES)
.addValueEventListener(roomsListener)
}
// 5.
private fun updateRooms(dataSnapshot: DataSnapshot) {
rooms.updateRooms(dataSnapshot)
otherRooms.updateOtherRooms(rooms)
ownRoom.updateOwnRoom(rooms)
currentRoom.updateCurrentRoom(rooms, ownRoom)
}
}
To demonstrate, the listener firebase initialization code was slightly simplified (the reconnect function was removed). Let's take a look at the points of what is happening here:
- the repository is designed as a singleton (in Kotlin, it is enough to replace the class keyword with object);
- initialization of LiveData objects;
- ValueEventListener is declared as a variable in order to avoid re-creating an anonymous class in case of reconnection (remember, I simplified initialization by removing reconnection in case of disconnection);
- initialization of ValueEventListener (if the data in Firebase changes, the listener will immediately execute and update the data in LiveData objects);
- Updates to LiveData objects.
The functions themselves are moved to a separate FirebaseRoomRepositoryPresenter file and decorated as extension functions.
fun MutableLiveData>.updateOtherRooms(rooms: MutableList) {
this.postValue(rooms.filter { !it.isOwnRoom() })
}
An example of the extension function from FirebaseRoomRepositoryPresenter
Also for a general understanding of the picture I will give a listing of the Room object.
// 1.
data class Room(var number: String = "",
var nickName: String = "",
var email: String? = null,
var imgSmall: String? = null,
var imgOffline: String? = null,
var imgFree: String? = null,
var imgWait: String? = null,
var imgBusy: String? = null,
var events: List = emptyList()) // 2.
- Data class. This modifier automatically generates and overrides the toString (), HashCode (), and equal () methods. There is no longer any need to redefine them yourself.
- The Events list from the Room object. It is this list that is required to update the data in the dial library.
All Repositories classes are hidden behind the facade class.
object Repository {
// 1.
private val firebaseRoomRepository = FirebaseRoomRepository
// .........
/**
* Rooms queries
*/
fun getOtherRooms() = firebaseRoomRepository.otherRooms
fun getOwnRoom() = firebaseRoomRepository.ownRoom
fun getAllRooms() = firebaseRoomRepository.rooms
// 2.
fun getCurrentRoom() = firebaseRoomRepository.currentRoom
// Другие репозитории
// .......
}
- Above you can see a list of all used repository classes and second-level facades. This simplifies the general understanding of the code and demonstrates a list of all connected repository classes.
- A list of methods that return references to LiveData objects from the FirebaseRoomRepository. Kotlin's setters and getters are optional, so you don't need to write them unnecessarily.
Such an organization allows you to comfortably fit from 20 to 30 requests in one root repository. If your application has more requests, you will have to divide the root facade into 2 or more.
ViewModel
BaseViewModel is the base ViewModel from which all ViewModels are inherited. It includes one single currentRoom object, used universally.
// 1.
open class BaseViewModel : ViewModel() {
// 2.
fun getCurrentRoom() = Repository.getCurrentRoom()
}
- Маркер open означает, что от класса можно наследоваться. По умолчанию в Kotlin все классы и методы являются final, т.е. от классов нельзя наследоваться, а методы нельзя переопределять. Это сделано для защиты от случайных несовместимых версионных изменений. Приведу пример.
Вы разрабатываете новую версию библиотеки. В какой-то момент по той или иной причине вы решили переименовать класс или изменить сигнатуру какого-то метода. Изменив его, вы случайно создали несовместимость версий. Упс… Если бы вы наверняка знали, что метод может быть кем-то переопределён, а класс унаследован, вы наверняка были бы более аккуратным и вряд ли бы выстрелили себе в ногу. Для этого в Kotlin по умолчанию всё объявлено как final, а для отмены существует модификатор «open». - The getCurrentRoom () method returns a link to the LiveData object of the current room from the Repository, which, in turn, is taken from the FirebaseRoomRepository. When this method is called, the Room object will return containing all the information about the room, including a list of events.
In order to convert data from one format to another, we will use transformation. To do this, create a MainFragmentViewModel and inherit it from BaseViewModel .
MainFragmentViewModel is a derived class from BaseViewModel. This ViewModel is used only in MainFragment.
// 1.
class MainFragmentViewModel: BaseViewModel () {
// 2.
var currentRoomEvents = Transformations.switchMap(getCurrentRoom()) {
val events: MutableLiveData> = MutableLiveData()
// some business logic
events.postValue(it?.eventsList)
events
}
// 3.
val currentRoomEvents2 = MediatorLiveData>().apply {
addSource(getCurrentRoom()) { room ->
// some business logic
postValue(room?.eventsList)
}
}
}
- Note the lack of the open modifier. This means that no one inherits from the class.
- currentRoomEvents — объект, полученный с помощью трансформации. Как только объект текущей комнаты изменится, выполнится трансформация и объект currentRoomEvents обновится.
- MediatorLiveData. Результат идентичен трансформации (приведён для ознакомления).
The first option is used to convert data from one type to another, which is what we needed, and the second option is needed to execute some business logic. However, data conversion does not occur. Remember that android import in ViewModel is not valid. Therefore, I start additional requests from here or restart services as necessary.
Important notice! In order for the transformation or mediator to work, someone must be subscribed to them from fragment or activity. Otherwise, the code will not be executed, because no one will expect a result (these are observer objects).
Mainfragment
The final step in converting data to result. MainFragment includes a dial library and a View-Pager at the bottom of the screen.
class MainFragment : BaseFragment() {
// 1.
private lateinit var viewModel: MainFragmentViewModel
// 2.
private val currentRoomObserver = Observer> {
clockView.updateArcs(it)
}
override fun onAttach(context: Context?) {
super.onAttach(context)
// 3.
viewModel = ViewModelProviders.of(this).get(MainFragmentViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_main, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// 4.
viewModel.currentRoomEvents.observe(viewLifecycleOwner, currentRoomObserver)
}
}
- Предварительная инициализация MainFragmentViewModel. Модификатор lateinit указывает на то, что мы обещаем инициализировать этот объект позже, до того, как будем использовать. Kotlin старается защитить программиста от некорректного написания кода, поэтому мы должны либо сразу сказать, что объект может быть null, либо поставить lateinit. В данном случае ViewModel обязательно должно быть инициализировано объектом.
- Observer-listener для обновления циферблата.
- Инициализация ViewModel. Обратите внимание, это происходит сразу после того, как фрагмент прикрепился к activity.
- После того как activity будет создана, мы подписываемся на изменения объекта currentRoomEvents. Обратите внимание, что я подписываюсь не на жизненный цикл фрагмента (this), а на объект viewLifecycleOwner. Дело в том, что в support library 28.0.0 и AndroidX 1.0.0 обнаружился баг при «отписывании» observer. Для решения этой проблемы была выпущена заплатка в виде viewLifecycleOwner, и Google рекомендует подписываться именно на него. Это исправляет проблему зомби-observer-а, когда фрагмент умер, а observer продолжает работать. Если вы всё ещё используете this, обязательно замените его на viewLifecycleOwner.
Thus, I want to demonstrate the simplicity and beauty of MVVM and LiveData without using data binding. Please note that in this project I violate the generally accepted rule by placing LiveData in the Repository due to the specifics of the project. However, if we moved them to the ViewModel, the overall picture would remain unchanged.
As a cherry on a cake, I have prepared for you a short video with a demonstration (names are smeared in accordance with security requirements, I apologize):
Summary
As a result of the application in the first month, some bugs were revealed in the display of cross rallies (Outlook allows you to create several events at the same time, while our system does not). Now the system has been working for 3 months. Errors or failures are not observed.
PS Thanks jericho_code for the comment. In Kotlin, you can and should initialize List <> in the model using emptyList (), then an extra object is not created.
var events: List = emptyList() // функция возвращает ссылку на синглтон EmptyList
var events: List = ArrayList() // создается лишний объект