Migrating FOSDEM Companion to Kotlin
This is a modal window.
Das Video konnte nicht geladen werden, da entweder ein Server- oder Netzwerkfehler auftrat oder das Format nicht unterstützt wird.
Formale Metadaten
Titel |
| |
Serientitel | ||
Anzahl der Teile | 490 | |
Autor | ||
Lizenz | CC-Namensnennung 2.0 Belgien: Sie dürfen das Werk bzw. den Inhalt zu jedem legalen Zweck nutzen, verändern und in unveränderter oder veränderter Form vervielfältigen, verbreiten und öffentlich zugänglich machen, sofern Sie den Namen des Autors/Rechteinhabers in der von ihm festgelegten Weise nennen. | |
Identifikatoren | 10.5446/47485 (DOI) | |
Herausgeber | ||
Erscheinungsjahr | ||
Sprache |
Inhaltliche Metadaten
Fachgebiet | |
Genre |
00:00
ProgrammschemaHumanoider RoboterQuellcodeGoogolUnrundheitMAPGüte der AnpassungHumanoider RoboterBildschirmmaskeBitSchedulingKartesische KoordinatenApp <Programm>Open SourceQuellcodeSoftwareentwicklerMultiplikationsoperatorInformationsspeicherungYouTubeGoogolDigitale PhotographieComputeranimation
01:23
MathematikVektorraumGoogolAggregatzustandKomponente <Software>ProgrammbibliothekLesezeichen <Internet>Architektur <Informatik>DatenbankApp <Programm>CodeRefactoringHumanoider RoboterProgrammbibliothekDatenbankEinfache GenauigkeitAppletGeradeGüte der AnpassungSoftwareentwicklerTwitter <Softwareplattform>SystemaufrufPunktFaktor <Algebra>VersionsverwaltungNeunzehnComputeranimation
02:27
Umsetzung <Informatik>AppletElektronische PublikationCodeProzess <Informatik>QuellcodeKlasse <Mathematik>DatenbankUmsetzung <Informatik>PunktSystemprogrammDatenbankVersionsverwaltungElektronische PublikationKlasse <Mathematik>App <Programm>Endliche ModelltheorieProjektive EbenePlug inKomplexitätsklasseSichtenkonzeptHumanoider RoboterBitAbstimmung <Frequenz>Rechter WinkelCodierungComputeranimation
03:15
CodeRefactoringHumanoider RoboterUmsetzung <Informatik>CodeMathematikCodierungArithmetischer AusdruckOrdnung <Mathematik>Befehl <Informatik>Schnitt <Mathematik>SystemaufrufBitrateKlasse <Mathematik>SystemprogrammAppletNichtlinearer OperatorComputeranimation
03:56
IndexberechnungZeichenketteAppletDigitalfilterStandardabweichungProgrammbibliothekMaßerweiterungFunktion <Mathematik>Humanoider RoboterMaßerweiterungAppletCodeKlasse <Mathematik>Ordnung <Mathematik>SystemprogrammFunktionalZeichenketteStandardabweichungProgrammbibliothekKnoten <Statik>TopologieMeterComputeranimation
04:43
FehlermeldungEreignishorizontMultiplikationsoperatorAggregatzustandEinfache GenauigkeitKlasse <Mathematik>ResultanteAppletNichtlinearer OperatorObjekt <Kategorie>CodeDatenbankComputeranimation
05:14
Klasse <Mathematik>Objekt <Kategorie>FehlermeldungTermCompilerCASE <Informatik>Klasse <Mathematik>ResultanteObjekt <Kategorie>Befehl <Informatik>Computeranimation
05:38
EreignishorizontIndexberechnungLaufwerk <Datentechnik>AppletWeg <Topologie>Mailing-ListeSystemtechnikVollständiger VerbandGruppenkeimOrdnung <Mathematik>ZeichenketteMultiplikationKlasse <Mathematik>DatenmodellFitnessfunktionFortsetzung <Mathematik>MathematikGeradeEinfache GenauigkeitKlasse <Mathematik>Prozess <Informatik>Endliche ModelltheorieZeichenketteArithmetischer AusdruckMinkowski-MetrikCodeCoprozessorAbfrageDatenbankComputeranimation
06:36
ZeichenketteVerschlingungFahne <Mathematik>EreignishorizontHill-DifferentialgleichungKlasse <Mathematik>Endliche ModelltheorieVerschlingungWeb-SeiteKlasse <Mathematik>TextbausteinCodeCompilerPlug inOrdnung <Mathematik>ImplementierungMinimumHash-AlgorithmusKonstruktor <Informatik>DatenfeldMereologieBruchrechnungPhysikalischer EffektComputerspielComputeranimation
07:55
BeobachtungsstudieHumanoider RoboterMaßerweiterungVerschlingungKlasse <Mathematik>Lesen <Datenverarbeitung>Inverser LimesEreignishorizontCodierungMultiplikationsoperatorSystemzusammenbruchPlug inCompilerCodeComputeranimation
08:31
EreignishorizontDateiformatCodeEreignishorizontApp <Programm>FunktionalMultiplikationsoperatorKlasse <Mathematik>AppletCompilerZeiger <Informatik>BildschirmmaskeProzess <Informatik>DateiformatAnfangswertproblemCASE <Informatik>XML
09:14
DatentypAppletCodeDatenfeldAnfangswertproblemZeiger <Informatik>LaufzeitfehlerAusnahmebehandlungKonfiguration <Informatik>PunktZweiDruckverlaufMultiplikationsoperatorCASE <Informatik>Kategorie <Mathematik>AuswahlaxiomTwitter <Softwareplattform>Computeranimation
10:11
EreignishorizontHumanoider RoboterMaßerweiterungEreignishorizontProgrammbibliothekOrdnung <Mathematik>MaßerweiterungHumanoider RoboterSchnittmengeFramework <Informatik>MultiplikationsoperatorDatenfeldParametersystemGoogolCASE <Informatik>Schnitt <Mathematik>Computeranimation
10:52
SichtenkonzeptDatenmodellGeradeEinfache GenauigkeitTextbausteinMailing-ListeKlasse <Mathematik>SichtenkonzeptService providerMultiplikationsoperatorCodeRechter WinkelKategorie <Mathematik>
11:23
Weg <Topologie>SchedulingTransformation <Mathematik>RechenwerkPhysikalisches SystemBoolesche AlgebraStrom <Mathematik>MaßerweiterungFunktion <Mathematik>Ordnung <Mathematik>Transformation <Mathematik>CodeMultiplikationsoperatorFunktionalMapping <Computergraphik>MaßerweiterungMAPPhysikalischer EffektKette <Mathematik>AppletComputeranimation
12:01
ZeichenketteFunktion <Mathematik>KoroutineAnwendungsspezifischer ProzessorSystemaufrufZeichenketteMusterspracheMultiplikationsoperatorQuelle <Physik>CASE <Informatik>AppletHumanoider RoboterCodeKoroutineComputeranimation
12:38
ThreadKontextbezogenes SystemFunktionalElektronische PublikationSchedulingp-BlockKoroutineThreadKontextbezogenes SystemKomplex <Algebra>SystemaufrufObjekt <Kategorie>Computeranimation
13:25
Syntaktische AnalyseParserE-MailFolge <Mathematik>KoroutineMenütechnikVariableSyntaktische AnalyseFolge <Mathematik>AggregatzustandMultiplikationsoperatorGarbentheorieGamecontrollerPunktLoopAppletDatenbankCodeFunktionalHalbleiterspeicherKomplex <Algebra>ZustandsmaschineEreignishorizontInformationsspeicherungElektronische PublikationStandardabweichungApp <Programm>Ordnung <Mathematik>ParserMailing-Listep-BlockComputeranimation
15:27
MenütechnikSichtenkonzeptOvalVererbungshierarchieAggregatzustandGarbentheorieStrom <Mathematik>FaserbündelKoroutineMathematische LogikMenütechnikVariableOrdnung <Mathematik>GarbentheorieSystemzusammenbruchWeb logTransaktionApp <Programm>CodeTouchscreenMathematikEreignishorizontKoroutineAbgeschlossene MengeBitProzess <Informatik>AggregatzustandInstantiierungGoogolSystemaufrufDifferenteComputeranimation
17:40
Analytische FortsetzungObjekt <Kategorie>SichtenkonzeptStrom <Mathematik>MenütechnikMaßerweiterungCodeCASE <Informatik>MenütechnikAbgeschlossene MengeGeradeMehrwertnetzAnwendungsspezifischer ProzessorEvoluteFunktionalBitWechselsprungTopologieTransaktionAusnahmebehandlungMaßerweiterungGarbentheorieMultiplikationsoperatorComputerspielDreiecksfreier GraphMathematikPunktSystemaufrufStandardabweichungKoroutineZahlenbereichNavigierenp-BlockComputeranimation
20:21
GeradeCodeAppletHumanoider RoboterCodeGeradeKoroutineBitElektronische PublikationAppletApp <Programm>MultiplikationsoperatorUmsetzung <Informatik>Rechter WinkelVersionsverwaltungCASE <Informatik>Web SiteFunktionalDiagramm
21:47
Jensen-MaßApp <Programm>DifferenteTypentheorieWidgetVorlesung/Konferenz
22:40
PunktwolkeFacebookOpen SourceDiskrete-Elemente-Methode
Transkript: Englisch(automatisch erzeugt)
00:07
Yeah, and I'm happy to introduce Christophe, that is going to talk about migrating FOSDEM companion to Kotlin. So, give him up his big round of applause.
00:22
Good morning everyone, I hope you can hear me. So, I'm very honored to be present at the first ever Kotlin Dev Room at FOSDEM. My name is Christophe Baes, I'm a freelance Android developer from Brussels, and during my spare time I make a few Android applications like FOSDEM companion,
00:42
and I'm going to talk about this app today. So, FOSDEM companion is currently the most popular FOSDEM schedule app for Android. There are other ones, so I invite you to test them, I don't have a monopoly, but of course it's FOSDEM, so the source code is available on GitHub,
01:00
and everybody can check it out. The application is available in binary form on the Google Play Store, and there are about 4,000 active users every year during FOSDEM. It's also published on the open source store, Android, but I don't have any stats, I don't know how many people are using it from this store.
01:22
So, I will start with a bit of history of the app quickly. So, this is actually a picture of the app running on the first ever Android phone, the HTC G1. So, the app has been introduced in 2014, so it's already six years old, and it's been created as a rewrite from me
01:42
of an existing app called FOSDEM Android. And from the start, I really wanted this app to be like a showcase of the good Android development practices. Over the years, I continued to update the app to introduce a few new features and to make the code up to date with the latest Android trends.
02:00
Most notably, last year in 2019, I migrated the database layer because it's really a database-centric app. So, I updated the database layer to make use of Room, the Room library from Google. And this year, I introduced a dark theme, and most importantly, I did the biggest refactoring ever because I rewrote the whole code in Kotlin.
02:22
So, there is not a single line of Java code left. So, how did I manage to do that? So, if you are running version 2.0, you are not running the Kotlin version of the app. So, I did that in the course of one week. It was a very busy week.
02:40
I basically, I converted the project file by file using the Kotlin plugin from Android Studio with the automatic conversion as a starting point. And I proceeded like this. So, I started with simple classes with no dependencies, like the utility classes, the model and entities, and then I moved on to the database layer,
03:00
the view model, the fragments, and finally the activities. I think this is the right way to do it because when you arrive at the more complex classes with more dependencies, then you can rely on the lower layers, which are already converted in Kotlin. And I was actually surprised by the quality of the codes generated by the automatic converter.
03:22
And even after the automatic conversion, the IDE kept suggesting me a few changes to make the code cleaner. For example, here, it proposed me to change the if statement into an if expression because in Kotlin, if is actually an expression. And then right after that, it proposed me another change to use the LV separator, and now the code
03:42
is even more compact and readable than before. But of course, this is not enough, and I had to rewrite big portions of the code by hand in order to really leverage all the Kotlin features that are not available in Java. So, let's take a look at a few examples where Kotlin shines compared to Java.
04:02
So, first of all, in many Android apps, you always have utility classes like this, string utilities, for example. And usually, you put a bunch of methods there. You don't really know how to organize them, and sometimes they can get quite complex. So, I managed to refactor most of the utility classes
04:20
into top-level extension functions when it makes sense. For example, here, a simple utility method on a string. And also, I leveraged the Kotlin standard library, which has much more features than the Java standard library in order to simplify the code. So, here I'm using the filter not function to just make this function much simpler than before.
04:44
Another example where Kotlin shines is, for example, this class in the legacy code base in Java. I had this single class which represents the result of the download operation. It's actually a single class which represents three different states. So, it represents either a success with a value
05:02
or an error, or the fact that the database is already up to date. Well, if you try to represent three states with a single class in Java, the code can get quite messy and complex. Well, in Kotlin, of course, the right way to do it is to use CIF classes. So, now I have one subclass for each state.
05:20
It's much cleaner, and when you have a constant state, you can just use objects which use a single term. And additionally, the compiler will also check that you are testing all the cases successfully when you are using the result in a when statement, for example. Now, our final example is the database layer.
05:43
So, like I said earlier, I'm using the Room database library. So, most of the database code is generated automatically by the annotation processor from Room, but I still have to write the SQL queries by hand. And most of the SQL queries are actually very complex,
06:00
like you can see here, and they don't fit on a single line, of course. So, in Java, you have to write it like this. You have to concatenate a bunch of strings, and you have to not forget to put a space at the beginning of each line. Otherwise, the query will not compile. And in Kotlin, you can simply use multi-line strings. So, it makes these SQL expressions much easier to maintain.
06:24
You can easily indent them as well. Now, let's move on to the model classes. So, the model and the entities are actually the classes where the most dramatic changes were introduced, as you will see now. So, this is a typical model class from the legacy code base in Java.
06:43
So, it represents a link at the bottom of a talk. A detail page. And as you can see, you have a bunch of fields, and then for each field, you have to write the getter and setter method. So, it's a lot of boilerplate code. This is the rest of the class. It's not over. So, I also have to overwrite equals and hash code by hand.
07:03
And then finally, because this class implements Parcelable, I also have to write all the Parcelable code by hand in order to properly serialize this class. And now in Kotlin, yeah, it's much simpler, as you can see. This is the same class as before, but at a fraction of the size.
07:21
So, basically, you still have the same fields, but now they are declared with a short syntax in the primary constructor. And because the class is a data class, then these fields will be part of the automatically generated implementation for equals and hash code. I don't have to write it anymore.
07:41
And also, because this class is annotated with addParcelize, then the Parcelize compiler plugin will actually generate the Parcelable implementation as well for these fields. Also, I forgot to say that this class is now read-only. Before, it was mutable.
08:01
So, now it's now safer than before. If you want to learn more about Parcelize, I suggest you to read this article I wrote last year. It's quite big, but it's a deep dive into the future, analyzing the compiler code and the limitations of the plugin and how you can overcome them.
08:21
It's quite a big read, but don't be afraid. It's worth it. So, now let's talk about null safety. Well, it turns out that in the previous code base, even though it was quite stable, there were still a few places where the app could actually crash. For example, here, I was not testing if an event had a start time.
08:42
It's possible that an event doesn't have a start time if it's not assigned to a time slot yet. And in this case, the app in Java would just crash because if you pass a null value to the didFormat.format function in Java, it just crashes. But after I converted that class to Kotlin,
09:01
now the compiler doesn't let me compile this code anymore. It forces me to do the null check. And now the code is safe and all is well. So, in general, I prefer to always use non-null and read-only fields when possible
09:20
because when a field is non-null, you don't have to check for nullability, of course. But you have to assign an initial value. So, when you can assign the initial value immediately, it's simple, but sometimes you can't always do that. So, if you can't assign an initial value immediately, you have two choices. Either you can use latent with a var, which is not read-only,
09:42
or you can use a delegated property, which is lazy, and in that case, it's read-only. I strongly recommend you to use the second option because if you use latent, actually, it can be quite dangerous. So, for example, if you forget to assign a value to the latent before you read it for the first time,
10:04
then the code will compile just fine, but then it will crash at runtime with a null pointer exception. So, we are back to the Java days. So, that's what I did here. For example, in all the fragments, every time I tried to read an argument, like here in this example, I read an event in the field, I used lazy,
10:23
and in this case, the field is non-null and read-only. Now, let's talk about Android KTX. So, Eric already introduced what it is. Basically, it's a set of libraries provided by Google which provides Kotlin extensions for the Android framework
10:43
and also for the Jetpack libraries by Google in order to make these APIs more Kotlin-friendly. So, here is like the most common example is the ViewModels delegated property.
11:01
I don't know if you're familiar with the ViewModels provider API in Java, but it's quite a lot of boilerplate code usually, and thanks to this, you can just write in a single line of code. You can fetch your ViewModel, and it actually works like a lazy which is specialized for ViewModels. So, the first time, it will create the right ViewModel class, and then it will cache it.
11:24
Another example where KTX was quite helpful is this kind of code. So, it's quite messy, as you can see. I needed to do a few transformations on LiveData because I'm not using Eric's Java in the project, and because with the Java API, you can't really chain the transformations.
11:42
So, I had to introduce a temporary variable here in order to chain two switch maps. But now, in Kotlin with KTX, it's much simpler because we have these switch map and map extension functions, so now I can chain them easily, and the code is still understandable and readable.
12:02
And the final example where KTX really helped make the code cleaner is for the Spannable String Builder API. I don't know if you know this API, this is one of the worst API on Android if you use it in Java, and if you move to KTX, then Google actually fixed the API
12:20
by using another kind of pattern, which is like a DSL to build your Spannable String. So, in this case, it's much simpler, as you can see. And now, let's talk about coroutines because everybody likes coroutines, and that's what you've all been waiting for, probably. So, yes, I like coroutines too, and I try to integrate them in the app,
12:41
and I will try to show you this with three main examples. So, what I like the most about coroutines is how easy and safe it is to switch threads. So, for example, here I'm downloading my schedule XML file from a background thread, and I can easily retrieve the value from another thread because wiscom.txt is a suspending function.
13:03
But then, it turns out that if I call this function on the main thread, it will start on the main thread, then it will run my block on the background thread, and it will then automatically return back to the main thread, thanks to context preservation. And this, in my opinion, makes the thread switching
13:21
safer and simpler than with RxJava, for example. Another example where I used coroutines in the app is to simplify the main XML parser, which reads all the talks from the file and stores them in the database. So, I won't go into detail of the legacy code,
13:41
but basically, you need to know this. The parser is not building a big list with all the talks and returning them in one go. Actually, it returns an iterable. So, the consumer of the iterable is able to read the events one by one and insert them one by one in the database, which is better for performance
14:02
and also better for memory usage. In order to do that in Java, I was forced to create a complex state machine, and the parsing code was spread into different methods. I also had to maintain a global state to know exactly where I am in my parsing. And now, this complex code is just gone,
14:22
and instead, I can use a coroutine function, which is called sequence, the sequence builder. So, thanks to sequence, which is like a lesser known feature of the Kotlin standard library, you can actually build a coroutine, like a suspending function, that is creating a sequence,
14:45
and sequence is very similar to iterable, like we saw before. So, every time you call yield, it will actually suspend the parser, give back control to the consumer of the sequence, which can then insert even into the database,
15:01
and then, when it will go next on the iterator, then the coroutine will resume at the yield point, and the loop will continue. So, you can put all the code in one block. And now, you don't need to maintain a global state either, because you can just use local variables,
15:20
and they will be restored to their previous state when the coroutine resumes. And now, we'll talk about a final example. It's how I use coroutines to simplify the UI code. So, you may have noticed that, but in the app, if you select a menu item from the main menu,
15:41
it will not immediately switch to another section. It will actually wait for the menu to finish closing, and then it will switch. Why do I do that? It's because if I switch immediately, then the menu animation is actually a bit stuttering, a bit sluggish. So, when I introduced that change in Java,
16:01
actually, I made the code way more complex than before, because now I have to deal with a few different callbacks to make this work properly. So, the first callback you have to register is when you press the menu item. Then, you have to store the pending menu item into a global variable, which is already not great. And then, you have to trigger the closing of the menu.
16:23
Then, you have to implement the second callback, where you listen for the menu closing event. As soon as the menu is closed, you consume the pending menu item, and you navigate. But it's not over, because actually, on Android, if you perform a fragment transaction after onSaveInstanceState, it's not allowed.
16:43
It will make the app crash. And it turns out that that's what I do when I switch to another section. I actually do a fragment transaction. So, in order to prevent the app from crashing, if the user actually leaves the screen before the menu finishes closing, then I cancel the navigation when the user leaves.
17:02
So, I have to override a third callback. So, in that case, what I do is just, I cancel the pending navigation menu item, and I restore the menu to the previous state. So, we have three callbacks and a global variable. So, it's really not clean. Can we use coroutines to simplify that?
17:21
Well, it turns out we can. So, I took my inspiration from this blog post that has been published in December by Chris Baines, from, who is working for Google. It's a very interesting article, which basically explains how you can use coroutines to orchestrate your UI and your animations.
17:41
So, that's exactly what I did. I created an extension function on the drawer layout to close the menu and suspend the coroutine until the menu is actually closed. And then, in that, sorry, to create that suspending function, I'm using the suspendCancelableCoroutine,
18:01
which is the standard coroutine builder function. I create a listener function, and I add it to my drawer menu. And if we take a closer look into that listener object, it actually overrides two methods. So, first of all, we resume the coroutine when the drawer menu is closed,
18:22
but also, it turns out that the user can also grab the menu before it closes, and in that case, it will actually cancel the closing of the menu. In that case, what we do is just, we intercept that event, and we cancel the coroutine, because there is no point in navigating to the new section if the user canceled it.
18:44
And now, thanks to this change, the code becomes much simpler. We only have a single callback to write. In the single callback, we launch a coroutine, and we just call our new function to wait for the drawer to close, and after that, we navigate. That's it, all the code is in a single block.
19:03
But there is a little bit of magic here. To launch our coroutine, we use a special scope. Eric already talked about that in his talk, so we use the lifecycle scope of the activity. Thanks to that scope, the coroutine will be canceled automatically when the activity is destroyed.
19:22
But we also use another magical function, which is launchWhenStarted. And thanks to that function, actually, the coroutine will only be active when the activity is actually visible. And the rest of the time, the coroutine will just be suspended. So it turns out that if the user leaves the activity
19:40
before the menu closes, then the coroutine will just stay suspended, and it will wait for the user to come back and then perform the fragment transaction. So now it is safe to perform the fragment transaction in that block. And the code is better than before because we are not canceling the navigation. We are just postponing it until the user comes back.
20:02
And also, finally, we still catch the cancellation exception in case the user grabs the menu. And in that case, we can reset the menu to the previous item and just cancel the navigation. In conclusion, I will just show you a few numbers.
20:21
So the lines of code. We went from a bit more than 8,000 lines of code in Java to a bit more than 6,000 lines of code in Kotlin. And the code is now simpler and safer. So basically, we reduced our code base, and we have 24% less code to maintain,
20:40
which is good, right? And now what about the costs? Well, if we take a look at the DEX file size, which is the file which contains the bytecode, not the resources, after optimizing it with R8, then it's only 12% bigger. So the app is a bit bigger, but the code is much safer and also a bit smarter
21:04
as you have seen with the coroutines. I suspect that the cost will be even lower for a bigger code base because 8,000 lines of code is not that big, and it will be less than 12%. So overall, I'm very happy with the conversion, and I think it was worth the time.
21:22
Thank you very much. So I'm listening for questions. If you have some.
21:43
I know it's very alpha, but did you consider using Compose for the UI? Rewriting the UI as well in Jetpack Compose, with composables. Well, Compose is really an alpha version, and it's very, very different so basically I would have to rewrite the app.
22:02
I mean, in this case, I kept the functionality and everything, but if I was using Compose, it would be a different app, of course. I could not reuse any UI widgets, but yeah, I'm taking a look at Compose, and that's what we'll all be using in a few years, I'm sure of it.
22:24
Other questions? Thank you very much again. Thank you.