We update the content without going through a repeat review in stores

We update the content without going through a repeat review in stores / Habr

The scheme of operation of the Server driven view

Hello everyone, I’m Dima Avdeev from Tutu, our team is developing applications with 20M installations. I’ll tell you how you can update the application without rolling out the release. For example, when we want to quickly convey coronavirus restrictions to users.

Below is the implementation in SwiftUI and Kotlin (but you can use UIKit and the server language adopted by your team), and in the GitHub repository at the end of the article you will find the server and application code for detailed study.

A post based on my report.

Table of contents

How it works

JSON in tree format will be sent from the server. There we will describe our layout. Then we natively render JSON on SwiftUI. If the user interacts with the button, a new HTTP call will be sent: we will add the button ID to the body of this call, as a result of which a new JSON will be returned from the server, and we will display it. The server can also send some additional data, and we will process it. And we can even provide logic like deep links.

Is it similar to MVI ?

In fact, this is a unidirectional MVI (Model View Intent) architecture. Model is the JSON data that comes to us from the server. View is our SwiftUI. Intent — the body of the new server call.

Here is a good video if you want to know more about MVI

Let’s make a screen with important information. Here is the layout of this screen. Our Server driven view structure is a SwiftUI structure. There, in the constructor, we pass the user ID and the server path by which we will request JSON for display.

ServerDrivenView(
       userId: "my_user_id",
       networkReducerUrl: "http://localhost:8081/important_reducer",
       autoUpdate: true
)

Let’s see how the server responds to this call. The tree is given away. For example, there may be a vertical container at the root, in which the first child is a picture, the second is a text.

rootContainer {
    image("http://localhost:8081/static/covid_test.png", 250, 250)
    text("Это ServerDrivenView")
}

In order not to form JSON with our hands, we made a special DSL wrapper – and you can change it, restart the server and immediately see the changes on the client.

But why Server driven view when there is…

  • You will be able to provide the native iOS interface familiar to the user, unlike WebView and Push. At the same time, the approaches do not contradict the Server driven view, they can be used together.
  • You will be able to raise a simple server yourself, check how the layout is displayed, edit it and debug it. You can allocate a separate microservice for this and deploy it in your infrastructure. It is more reasonable to give the logic of this service to mobile developers for implementation: we know better in what format to accept this layout and will be able to lay down compatibility and versioning.
  • At the same time, the server developer can change the UI and debug the logic of his feature on the server, without waiting for the iOS team to be released.
  • A/B tests can be managed from the server code, adding experimental options on the fly without feature toggle on the client.
  • All this is also applicable for Android applications — and one microservice can serve both platforms.

But there are nuances

The rules of Apple and Google allow the use of such updated views, but limited: do not change the basic functionality of the application.

The response time when pressing the buttons may increase if the client has a bad Internet connection. The load on the server infrastructure may increase, but we can optimize this by adding a caching layer, changing JSON to binary data, or changing the protocol to a websocket.

Finally, versioning. You need to make several server URLs, and the client, depending on the version, will access them. And the server is to handle everything so that old clients are not given unnecessary information.

Let’s look deeper

The SwiftUI structure has three states: boot state, normal state, and error state.

public var body: some View {
   switch myViewModel.myState.screen {
   case let loadingScreen as ServerDrivenViewState.ServerDrivenViewScreenLoading:
       Text("Loading...")
   case let normalScreen as ServerDrivenViewState.ServerDrivenViewScreenNormal:
       RenderNode(normalScreen.node, myViewModel.myState.clientStorage) { (intent: ClientIntent) in
           mviStore.sendIntent(intent: intent)
       }
   case let errorScreen as ServerDrivenViewState.ServerDrivenViewScreenNetworkError:
       VStack {
           Text("Сетевая ошибка")
           Text(errorScreen.exception)
       }
   default:
       Text("wrong ServerDrivenViewState myViewModel.myState.screen: (myViewModel.myState.screen)")
   }
}

The normal state calls the Render Node structure:it is needed to render any elements of our JSON. The list can contain all possible JSON objects — a vertical or horizontal container, text, rectangle, button, and so on.

The button takes in the Intent, aka the ID of the button that will be pressed.

public struct LeafButton: View {
   let nodeButton: ViewTreeNode.Leaf.LeafButton
   let sendIntent: (ClientIntent) -> Void
   public init(_ nodeButton: ViewTreeNode.LeafButton, _ sendIntent: @escaping (ClientIntent) -> ()) {
       self.nodeButton = nodeButton
       self.sendIntent = sendIntent
   }
   public var body: some View {
       Button(action: {
           sendIntent(SwiftHelperKt.buttonIntent(buttonId: nodeButton.id))
       }) {
           Text(nodeButton.text)
                   .font(.system(size: CGFloat(nodeButton.fontSize)))
       }
   }
}

The container iterates through all its children and calls the Render Node function again for each child. That is, in fact, it turns out recursion.

public struct ContainerHorizontal: View {
   let containerHorizontal: ViewTreeNode.Container.ContainerHorizontal
   public init(_ containerHorizontal: ViewTreeNode.ContainerHorizontal) {
       self.containerHorizontal = containerHorizontal
   }
   public var body: some View {
       HStack(alignment: containerHorizontal.verticalAlignment.toSwiftAlignment()) {
           ForEach(containerHorizontal.children) { child in
               RenderNode(child, ...)
           }
       }
   }
}

A container can also contain another container inside itself. This way we can render the entire tree: we have a global Render Node, and we describe each node separately in SwiftUI.

How convenient is it to change and update the view from the server in practice

Let’s make a screen with statistics of cases: we will have a city, a picture for the city, statistics for several days in vertical rectangles (green — few cases, red – a lot), plus a description of what restrictions are currently in effect in this city.

Let’s get a model of the city – the name, the picture, the statistics of the sick for a few days (then the server code on Kotlin will go).

data class City(
   val name:String,
   val img:String,
   val stat:List<Day> = List(7) {
       Day(
           it.toDayOfWeek(),
           (100..1000).random()
       )
   },
   val desc:String = createRandomDescription()
)

And a model for the day, it will be a text string “day of the week” and the number of cases on that day. We will generate the data randomly in the range from 100 to 1000.

data class Day(
   val dayOfWeek:String,
   val infected:Int
)

Now let’s insert the data into our card and make the statistics display for one city. We cycle through the days from the model city and display the number of cases.

fun NodeDsl.cityCard(city: City) {
   horizontalContainer(lightBlue) {
       verticalContainer {
           text(city.name)
           image(city.img, 100, 100)
       }
       verticalContainer {
           text("Статистика коронавируса", 15)
           horizontalContainer(contentAlignment = VAlign.Bottom) {
               city.stat.forEach {
                   verticalContainer {
                       text(it.infected.toString(), 14)
                       val normalized = it.infected / 1000.0
                       val red = (255 * normalized).toInt()
                       val green = 255 - red
                       val height = (60 * normalized).toInt()
                       rectangle(20, height, Color(red, green, 0))
                       text(it.dayOfWeek, 16)
                   }
               }
           }
           text(city.desc, 16)
       }
   }
}

Next, we iterate through all the cities in the loop and display cityCard(city).

val cities = listOf(
   City("Москва", moscowUrl),
   City("Санкт-Петербург", spbUrl),
   City("Владивосток", vladivostokUrl),
)
fun serverResponsePlayground(clientStorage: ClientStorage): ViewTreeNode {
   return rootContainer() {
       cities.forEach { city ->
           cityCard(city)
       }
   }
}

That’s all, we made the screen exclusively in the server code, we can make changes quickly enough and see how they are displayed on the client.

P.S. Details can be found in this GitHub repository: there you will find Android and iOS applications, a server and instructions for assembly and launch.

Thanks for your attention!

Unity 3D Development Outsourcing | IT Outsource Support

Ready to see us in action:

More To Explore

IWanta.tech
Logo
Enable registration in settings - general
Have any project in mind?

Contact us:

small_c_popup.png