Loading...
Home
  • Tech Blogs
  • Videos
  • Conferences
    • Droidcon News
    • Upcoming Conferences
    • Become a Partner
    • Past Events
    • Keep Me Informed
    • Diversity Scholarships
  • Community
    • droidcon Team
    • How to Hold a Droidcon
  • Android Careers
Sign In

Global CSS

droidcon News
Espresso Patronum - The Magic Of The Robot Pattern
By
Adam McNeilly
droidcon London 2019
Transcript
English
00:00
can everyone hear me okay cool so I'm
00:04
Anna McNeely I am a senior Android
00:06
engineer for a company called OkCupid
00:08
and today we are going to talk about the
00:11
robot pattern
00:12
more specifically what makes it so
00:14
magical and how it could help you write
00:16
better more maintainable espresso tests
00:19
when it comes to the robot pattern I
00:21
found the easiest way to explain why
00:23
it's helpful is to make sure we
00:25
understand the problem it solves first
00:27
so the way this talk is structured is
00:29
we're gonna explain espresso for any of
00:30
you who are unfamiliar with it show you
00:33
the problems you have when you just
00:35
follow the docs on espresso and then how
00:37
the robot pattern is gonna fix all of
00:39
those problems for you if you're
00:42
unfamiliar with espresso this is a UI
00:44
library UI testing library from Google
00:47
that allows you to write concise
00:48
beautiful and reliable Android UI tests
00:51
so if you have a feature in your app
00:53
even as simple as your login page and
00:55
every time you change your app and you
00:58
want to make sure the login page still
00:60
works to run the APK type in a username
01:03
password hit sign in make sure you get a
01:05
new page if you've ever wanted to
01:07
automate that entire flow have something
01:09
that will just type text into the phone
01:11
for you and click the buttons that's
01:14
what you would use espresso for when it
01:19
comes to developing your espresso tests
01:22
there are three classes that you need to
01:24
worry about few matress do actions and
01:28
view assertions and we're going to talk
01:30
about each of those view matters our
01:32
methods we call when we want to find a
01:35
view on the screen that matches some
01:37
conditions so if I want to interact with
01:40
a view that has a given ID I call with
01:42
ID R that ID that whatever or you can
01:46
find views that have a certain text that
01:48
you specify is it a focusable view is it
01:53
a checked view all sorts of conditions
01:56
like that but it's fine the view that
01:58
matches these conditions and then we
02:02
have our view actions and these are
02:04
called when we want to interact with a
02:07
view on the screen once we found it so
02:09
that could be typing text and through an
02:11
edit text
02:12
it could be scroll into a view if it's
02:14
not on the screen or scrolling to a
02:16
position in the recyclerview maybe
02:19
swiping left on a view or clicking on a
02:21
button and then we have view assertions
02:25
and these are for scenarios where I
02:28
found a view on the screen and now I
02:29
want to make sure that it matches some
02:32
conditions I have and there's a method
02:35
for that where you would just say
02:36
matches so what this is used for is I
02:39
might try to find a view on the screen
02:41
and I want to say that it matches that
02:43
it has some text inside of it you could
02:47
do is left out or is right as this
02:50
doesn't happen often but worth calling
02:52
out that it's there or maybe that a view
02:55
does not exist what so you have a layout
02:57
file that's different and say portrait
02:59
or landscape or phone versus tablet this
03:02
method is really helpful for making sure
03:04
that something's not created in a
03:06
certain layout and every espresso tests
03:11
sort of follows this simple pattern we
03:13
call on view with some view matcher
03:16
which gives us something called a view
03:17
interaction which we can then either
03:19
perform an action or check some
03:22
assertion a more specific example let's
03:26
say we have an app that you type some
03:28
text into an edit text and as you do
03:31
that it updates the textview on the
03:33
screen and do you want to make sure
03:34
using espresso that that works so the
03:37
first thing you'll do is call on view
03:38
with ID our that ID eat the input or
03:42
whatever you're out of Texas so you want
03:44
to perform an action typing for the text
03:46
and then entering your name and then you
03:49
want to find the view with the ID TV
03:51
output and check that it matches with
03:54
text Adam there's a lot of nesting here
03:57
but I think espresso does a good job of
03:58
clearly naming their methods so that as
04:01
we go through this left to right we can
04:03
kind of understand what each line is
04:05
doing something I'm going to work
04:09
through in this talk is to demonstrate
04:10
how we would write it as a test to fill
04:13
out a basic form like this so in this
04:15
sample we have a form that has first
04:17
name last name email phone number and
04:19
whether or not you are not in the emails
04:21
sort of simple registration form when
04:24
you click register it takes
04:25
a new screen that just shows you your
04:28
user details but they're also formatted
04:30
your first and last name are combined
04:32
together your format is phone is your
04:34
phone number is formatted with
04:36
parentheses around it and such and what
04:39
you see in this gif is actually an
04:40
espresso test running I'm not typing any
04:43
of this in myself this is all automated
04:47
now I want to put a little disclaimer in
04:49
before I go to the next slide because
04:50
it's very code heavy and I normally
04:53
don't like to do that my talks I don't
04:54
want to lose anyone but I did that on
04:56
purpose because I want you to see how
04:58
much code is involved in this but I
05:00
promise the whole talk isn't like that
05:01
so don't give up on me just because of
05:03
this one slide they use all of the code
05:06
for that happy path test we're on the
05:10
very first line we call our view with ID
05:12
easy first name perform type text Adam
05:14
and then we have to do the same thing on
05:16
the next one to type in my last name
05:17
email phone number and then we click and
05:20
then the bottom three lines are checking
05:23
that the output matches what I'd expect
05:26
it to be and this is a lot right this is
05:30
a lot of repetitive typing who makes it
05:32
easy to make mistakes because I'm
05:34
probably just gonna copy and paste from
05:36
line to line honestly when I first open
05:40
up this file and I look at this test
05:41
like I'm immediately taken aback because
05:43
I like don't know what's happening
05:47
because the lines are so long and I have
05:49
to read left to right all the way to
05:51
figure out that oh I'm typing Adam into
05:54
the first name edit text by the time I
05:56
have read the like seventh line in this
05:59
file are in this class method I've
06:02
probably already forgotten what it's
06:04
doing so that's just something to keep
06:06
in mind and it's also hard to discern
06:09
when you test leaving out a field like
06:12
that I've actually done that thankfully
06:15
I have like a helpful method name here
06:17
test missing email error and I can see
06:20
on the bottom that I'm checking for
06:21
error text but this is really sort of
06:24
indiscernible from the test before that
06:27
and again I might just copy and paste
06:30
things from the previous test and I'm
06:32
more likely to make a mistake because of
06:33
it maybe I've forgotten to change one of
06:36
the IDs or what I'm typing into it
06:39
and this is just another example of an
06:41
invalid field but then you throw that
06:44
all together and you just get this
06:45
really sort of large collection of
06:48
methods that are all blurred together
06:49
and they're really difficult to read
06:53
which is everything I'm going to talk
06:55
about the problem with this is this is
06:57
very verbose there's a lot of code
06:59
required to explain what my test is
07:01
doing and it's doing so in a way that's
07:04
not readable or not easy to read for
07:06
someone who didn't write the test
07:08
themselves it takes a long time to come
07:10
in and read those methods and understand
07:12
what they're actually testing but more
07:16
importantly beyond all the readability
07:18
concerns of this is these are difficult
07:20
to maintain because we have no
07:22
separation of concerns with our UI tests
07:27
highlight that let's consider how we
07:29
would consider separating concerns
07:31
inside our app itself you may use
07:34
model-view-presenter pattern or whatever
07:37
pattern you're familiar with but we do
07:39
that because the model has its own
07:40
responsibilities and it talks to the
07:43
presenter and if I ever want to change
07:45
my model say from a network to a
07:47
database ideally this is all behind one
07:50
interface where I can just swap out the
07:52
model and not have to change the
07:54
presenter right and that all seems fine
07:57
if you look on the right we have one
07:59
espresso test and our espresso test is
08:02
interacting with our view as you just
08:04
saw and that's fine if we have one
08:06
espresso test but ideally we had a lot
08:08
more than that and what will happen is
08:12
if you change something about your view
08:14
let's say some input changes from I
08:16
don't know like some edit text to maybe
08:19
you can make it a spinner or it becomes
08:21
like a date picker or something like
08:24
that that's going to break all of your
08:26
tests and all of your tests are going to
08:29
need to be updated even if it's as
08:31
simple as you change the identify out of
08:33
view if you don't use like Android
08:34
studios refactor you have to go into all
08:36
of your UI tests and change that
08:38
identifier so the solution to that
08:41
because we can't just wrap espresso
08:43
tests around an interface is to
08:46
introduce a new component in the middle
08:48
call it a robot your UI tests then know
08:52
how to interact with the robot and the
08:54
robot knows how to interact with the
08:56
view and so in the view changes the
08:59
robot changes and so this saves you a
09:02
lot of time when you go to change your
09:05
view and that you could do so in a way
09:07
that you know is not going to break all
09:09
of your tests which is really helpful
09:11
when I was bouncing around ideas about
09:13
this talk with a friend they told me
09:15
that tests discourage refactoring
09:18
because if your tests work a certain way
09:20
and you know that changing the UI is
09:22
gonna break all of your tests you don't
09:24
want to do it or you change the UI and
09:26
then you just delete all the tests
09:27
because they don't work so hopefully
09:30
this will help you fire up a solution to
09:31
that before we explain implementing a
09:36
robot though let's take a look at what
09:38
that would look like when we're done so
09:41
ideally we could just write a UI test
09:44
that is way more readable so this is
09:48
successful registration I created a
09:50
registration robot when I tell the robot
09:52
enter this first name and to this last
09:54
name email phone number click register
09:56
and then assert all of these outputs and
09:60
what I really like about this is this
10:02
allows you to write your tests as if
10:04
you're trying to tell a quality
10:06
assurance engineer what to do if you
10:08
were handing off your phone to QA to
10:10
test your new feature you're not gonna
10:12
tell them find the view with this ID
10:14
then perform the action of typing text
10:16
like that doesn't make any sense you're
10:18
going to tell them to do these steps so
10:21
that's a lot nicer it makes you test a
10:23
lot more readable and a lot more
10:24
composable which we'll see in a couple
10:27
slides so the first thing I do when I'm
10:32
creating a robot which is just a class
10:34
called registration robot is I throw all
10:37
of my view matches into a companion
10:39
object or if you're still using Java
10:41
these are just static fields but this is
10:44
every way to like find the views you're
10:46
interacting with because I might have
10:48
multiple methods later that interact
10:50
with first-name whether I'm inserting
10:53
text into them or invalidating the text
10:54
in them we'll throw whatever view
10:57
manager we have into one single place
11:00
then inside a robot we want to write one
11:03
method for each action that we want to
11:06
do and
11:06
screen so here a couple examples right a
11:09
method called first name and you could
11:11
call this and her first name maybe it's
11:13
more clear but I didn't think of that in
11:15
time to update the slides the first name
11:18
method returns a registration robot and
11:20
you can see that it just calls returned
11:23
this that's just the Builder pattern
11:25
that's what allows us as we saw to chain
11:27
these calls together and then we can go
11:30
and put our espresso or a robot methods
11:33
in any order we want and easily sort of
11:36
like plug and play with all of these
11:38
robot methods and so inside first name
11:41
we find a view with our first name
11:43
matcher that we defined earlier and then
11:46
perform some actions here is just a
11:48
little best practice I snuck in here
11:50
if you're writing espresso test for any
11:52
inputs try to clear the text
11:55
ideally there was nothing there before
11:57
but just in case type your text and then
11:59
close the keyboard if you don't do that
12:01
the keyboard can stay open and that can
12:03
sometimes break later steps and then
12:07
register works the same way it returns a
12:09
registration robot so I can put this
12:11
anywhere in my call inside the test I
12:14
find a view with the register
12:16
implementer I don't know why I call it
12:18
input but then we perform an action and
12:21
we click on it similarly we'll write one
12:25
method for each associate so I'll write
12:28
a method called a certain email display
12:29
give it the email I expect and then
12:32
within there I put it on do email
12:34
display matcher check matches with text
12:36
email I'm not going to be able to
12:39
completely get rid of this complicated
12:41
espresso code that has these really long
12:43
lines with nested methods but at least
12:46
here I can abstract it away from the
12:48
test file and make the test more
12:51
readable and now it's much easier to
12:54
understand a certain email display and
12:56
all the espresso code inside of it then
12:58
when I had seven lines of espresso
13:00
assertions back-to-back one small
13:03
difference here if you're using Java
13:05
instead of or if you're using Kotlin
13:08
instead of saying this returns
13:10
registration robot you can set this
13:11
equal to apply and it's basically the
13:14
same thing but it's just a little
13:16
cleaner less code
13:19
and then here's our implementation again
13:23
so again the nice thing about the
13:25
Builder pattern is this is really easy
13:27
to compose tests I've written a method
13:29
for every action I can take for all of
13:31
my inputs and I've got one for
13:33
everything that I want to assert and so
13:35
now if I want to take this and try to
13:38
make a negative test that's really easy
13:41
I just drop the email call and then put
13:44
a certain email error at the very end I
13:46
didn't have to copy and paste centum
13:49
liens find the right you know input ID
13:53
and change that this just becomes way
13:55
more composable and what you get out of
13:57
this is I've learned from a lot of
13:59
people that writing espresso tests takes
14:01
a lot of time so they might write their
14:03
happy path they might write one or two
14:05
air scenarios but if you take the time
14:08
to build out your robot to do everything
14:10
it's way more easier to test all sorts
14:13
of permutations of this form so if I
14:15
want to write a test for each of the
14:17
individual fields if I left it blank
14:19
that becomes way more easy to do with
14:21
something like this I'm again in Catalan
14:26
just a fun fact if you want to work some
14:28
Catalan magic you can write a
14:30
higher-order function called like
14:33
registration I haven't taken a function
14:35
that interacts on the registration robot
14:36
and then you can get this cool test at
14:39
the bottom where instead of all of the
14:41
Builder pattern calls would still look
14:42
very java asked you can write
14:45
registration put some brackets around it
14:46
and then call all of your methods in
14:48
there that was just a fun fact to
14:51
include if you're not familiar with it
14:54
so that's everything about why you need
14:59
a robot pattern how to implement one
15:02
what I want to share best practices I've
15:05
learned over time using the robot
15:07
pattern one really cool thing that you
15:12
can use the robot pattern for is
15:14
leveraging it for better test reporting
15:16
so if you want to write UI tests that
15:20
take a screenshot at every step or even
15:22
log that step somewhere because you've
15:25
already built a robot and you have every
15:27
step your test takes defined as its own
15:29
method you can just put that log in your
15:31
screenshot step
15:32
right inside that method this is way
15:35
more difficult if you had 12 different
15:37
full-length tests that are filling out
15:40
the form and then do you want to go put
15:42
a screenshot every time you enter the
15:44
first name if the copy and paste those
15:45
12 different types here you get to just
15:48
interject it once and that's really cool
15:51
and if you're interested in that I
15:52
recommend Sam Edwards droidcon NYC 2016
15:56
talk AB screenshots with espresso which
15:58
is where I first learned about the robot
15:60
pattern which he talked about for a few
16:02
minutes because of screenshots and I
16:04
realized there was way more to it then
16:05
he gave her credit for I also get a lot
16:10
of questions about navigation when it
16:12
comes to robots if you're navigating
16:13
from one screen to another when you do
16:16
that you want to use one robot per
16:18
screen so here we have a registration
16:21
robot that knows how to fill out the
16:23
form and they click register and then
16:25
when that goes to our user profile page
16:27
we want that to run all of the user
16:29
profile assertions and I need possible
16:31
actions that are on that screen and the
16:36
reason for that is it might sound nice
16:38
to chain robots together like when I
16:40
click register I know that's gonna take
16:43
me to the user profile screen so why not
16:45
just have register returned to user
16:47
profile robot but then we've coded
16:50
yourself into a corner where you're
16:51
unable to do negative tests so if I
16:53
don't fill out something in the form I'm
16:55
not supposed to go to the next screen so
16:58
if register returns me a user profile
17:00
robot then I can't call my assert email
17:03
error method any boy so that's just
17:06
something to keep in mind
17:07
it sounds nice to have that smooth
17:09
transition but we won't be able to write
17:12
any negative tests and that'll cause you
17:14
a lot of problems I also don't recommend
17:18
putting conditional logic in your robot
17:21
methods so it also might sound nice to
17:23
write one like assert the opt-in status
17:26
and pass in whether or not you expect
17:28
the user to be opted into emails and
17:30
then within there you find out okay if
17:33
they're after date I want is checked if
17:35
they're not I want is not checked and
17:38
then assert that the opt-in checkbox
17:41
that's a checkbox their matches
17:43
matter and why little work you just you
17:48
start putting this conditional logic in
17:49
here now you're wondering well who test
17:51
the test and if this doesn't work is it
17:54
because I have a bug in my app or did
17:56
this if statement break so if you're
17:59
going to do something like that just use
18:00
the separate methods create one called
18:02
assert app that in have that check for
18:05
it is checked and create one called
18:07
a certain opted out and have that check
18:09
for it's not checked now we've talked
18:16
about robot patterns in the context of
18:18
espresso and that's really helpful
18:21
because if our view changes we want all
18:23
of our tests to still work and we've got
18:28
this more or less ugly espresso code
18:31
that we're trying to hide behind
18:32
something more readable but actually
18:35
nothing I've talked about here with the
18:36
robot pattern is specific to UI testing
18:39
you can use this pattern inside your
18:42
unit test as well and you get all of the
18:44
same benefits of having more readable
18:46
tests and tests that are way easier to
18:48
update if you change one of your classes
18:51
and we're gonna walk through both of
18:53
those so here I want to show a view
18:58
model class that I want to unit test I
19:00
have a view model that takes in a
19:02
repository for fetching a user the view
19:06
model exposes some network state as a
19:08
live data and I have a method called
19:11
fetch user which will fetch the user
19:13
from the repository if it's successful
19:15
we're gonna set our state to load it if
19:18
it's not we're gonna set our state to
19:19
error
19:20
so let's write a unit test for that
19:23
prior to the robot pattern you may write
19:26
something like this we create a mock
19:28
repository using makino or mock kay or
19:30
whatever mocking library you like create
19:33
a sample user that we're going to return
19:35
and say whenever we factually user for
19:38
remark repository return this sample one
19:41
that I just created then we create our
19:44
view model with that Mac repository we
19:46
call fetch user which will just fetch
19:50
that user but because we're in the unit
19:52
test mocking it it's just gonna fetch
19:53
that sample user we created we get a
19:56
reference to the
19:57
current state this test observer is my
19:59
old like extension method and live data
20:02
to get like the latest value from a live
20:05
data in a unit test and then I'm going
20:07
to assert equals that Network States
20:09
loaded is equal to the current state and
20:13
I have all the same problems here when I
20:14
look at this test
20:16
I can't just look at it and within like
20:18
a few seconds figure out what's doing I
20:19
almost have to read every single line to
20:22
understand unless I write a really good
20:24
method name but here successful fetch I
20:27
don't know how helpful that is and we'll
20:32
look at this example in a second but to
20:35
also talk about the separation of
20:36
concerns here if I write a lot of tests
20:39
with this if I change my view model
20:42
state to be some rx type and not a live
20:45
data or whatever else I'm gonna have to
20:48
update every single one of these unit
20:50
tests so there's still a separation of
20:52
concerns problem here and our testing
20:55
well we can solve that by creating a
20:58
robot here too so here we have the user
21:00
view model robot it creates a block
21:03
repository and our view model using that
21:05
for us we have a method to mock the user
21:08
response so our robot knows how that
21:10
marking is supposed to happen
21:12
fetch user will just proxy that call
21:14
through into the view model and then we
21:16
have assert state where we tell it what
21:18
state we want to have and the robot is
21:21
responsible for getting that state from
21:23
the view model and then asserting that
21:25
they're equals implementing that robot
21:30
is pretty much the same we have a
21:33
successful fetch method at the bottom
21:35
that creates our sample user so the test
21:38
is still going to have some
21:39
responsibilities and I think that should
21:41
be like setting up the data that you
21:43
want so the test is responsible for
21:45
setting up here's our sample user that
21:47
we're going to fetch but then I could
21:49
just call test robot my user response
21:51
fetch user and then assert the state
21:54
there's something I also didn't hear was
21:56
I put it in our test robot as a late
21:60
init variable and then inside the set up
22:02
method I create the robot that's really
22:05
good because we want one robot for every
22:07
test you may get in a scenarios where
22:10
you
22:10
robot might have some kind of state
22:12
management in it and then you don't want
22:14
that state to bleed over from one test
22:16
to another so either use something like
22:19
this where you give it a new value every
22:21
time setup is called which anjuna is
22:23
called before each test or alternatively
22:26
inside the method here where I call test
22:30
robot I could just put the constructor
22:33
there myself but I like having one thing
22:35
that is easier to reference the loop
22:38
stick and so again now with my unit test
22:43
this becomes way easier to go in add an
22:45
error test I just need to add one day
22:48
method to my robot that's Mac user error
22:50
and then I could write a failure fetch
22:53
method by just plugging in that new
22:56
method into a builder pattern so let's
23:05
see this in action we've gone through
23:07
all the effort of creating a robot and
23:09
we've talked about it at a high level
23:11
like why this is helpful but let's look
23:14
at a real world example where this is
23:16
going to save you a lot of time so right
23:19
now I have two methods in here that
23:21
fetch like a successful fetch and a
23:25
failure fetch right and then I go
23:28
through loop okay oops okay I went too
23:32
far forward for whatever reason I
23:36
decided I'm gonna update my view model
23:38
and instead of exposing state as a live
23:40
data I'm gonna make it a behavior
23:42
subject and now previously I would have
23:47
to go into each of my successful fetch
23:49
and my failure fetch methods and any
23:51
other view model tests I have where um
23:53
the real world I might have dozens of
23:55
you view model tests and I'm gonna have
23:57
to change the way that I compare state
23:60
right not with this we just go into our
24:03
one robot method and instead of calling
24:05
the test observer I could just get the
24:07
state right for the behavior subject and
24:10
then I start those are equal and by
24:12
changing one line of my view model and
24:14
one line of my robot all of my tests
24:16
still pass so again there's two but
24:20
think about how many tests you hopefully
24:22
have and you've saved yourself
24:24
so much time as a result so let's recap
24:30
all of that utilize the robot pattern
24:34
for writing more readable and more
24:36
maintainable tests those are the two
24:38
biggest perks I think you get out of the
24:40
robot pattern but take advantage of it
24:44
if you want better test reporting
24:45
whether that's screen shot on your UI
24:47
test or maybe just logging to the
24:49
console so you understand what each test
24:51
is doing you pretty much get that for
24:54
free because you've already made one
24:56
method for every step you're gonna take
24:58
and make sure you don't code yourself
25:01
into a corner with additional complexity
25:03
by that I mean don't shave new robots
25:05
together and don't include any logic
25:08
inside these robot methods and while we
25:11
talked about UI testing as the biggest
25:15
use case for this and partially because
25:17
that's the way I was introduced to it
25:18
it's not specific to UI testing and it's
25:21
also not specific to espresso if you're
25:23
using any other UI testing framework the
25:26
same concept will still apply with those
25:28
as well and that's all I've got
25:32
if you want to see a sample app with
25:34
this as well as the slides I have a
25:36
github repo here
25:37
which shows what you saw earlier with
25:39
answering all the text into a form
25:41
clicking enter seeing a new page on that
25:44
sample repo I actually do include
25:46
screenshots if you want to see how
25:50
something like that would work so thank
25:52
you
25:59
I'll hang around up here if there are
26:01
questions I'd rather just do them up
26:02
here Thanks
droidcon News

Tech Showcases,

Developer Resources &

Partners

/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/home-details/EmployerBrandingHeader
EmployerBrandingHeader
https://jobs.droidcon.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/jobs-droidcon/jobs.droidcon.com
jobs.droidcon.com

Latest Android Jobs

http://www.kotlinweekly.net/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/kotlin-weekly/Kotlin Weekly
Kotlin Weekly

Your weekly dose of Kotlin

https://proandroiddev.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/pad/ProAndroidDev
ProAndroidDev

Android Tech Blogs, Case Studies and Step-by-Step Coding

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
Zalando

Meet one of Berlin's top employers

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
Academy for App Success

Google Play resources tailored for the global droidcon community

Follow us

Team droidcon

Get in touch with us

Write us an Email

 

 

Quicklinks

> Code of Conduct

> Terms and Conditions

> How to hold a conference

> FAQs

> Imprint

Droidcon is a registered trademark of Mobile Seasons GmbH Copyright © 2020. All rights reserved.

powered by Breakpoint One