Giter Club home page Giter Club logo

me's Introduction

Me

Please note, this is not an official Google repository. It is a Kotlin multiplatform experiment that makes no guarantees about API stability or long term support. None of the works presented here are production tested, and should not be taken as anything more than its face value.

Introduction

"Me" is a Kotlin Multiplatform playground for ideas that pop into my head around app architecture. These ideas typically center around state, and it's production; a repository of "what ifs?".

It follows the modern android development architecture guide, and attempts to extend it to envision what building apps will look like in the near future, with novel/experimental implementations of fundamental app architecture units including:

  • Navigation
  • Pagination
  • UI State production
  • Dependency injection
  • Persistent animation
  • Large screen experiences

All while targeting multiple platforms and meeting stringent product requirement constraints like:

  • No pull to refresh in the app. The app is always up to date.
  • Navigation is persisted between app restarts and device reboots.
  • Scroll position is preserved between app restarts and device reboots.

The app is a WYSIWYG editor for my personal website. The source for the backend can be found here.

Demo image

Some ideas explored include:

  • Mutators as abstract data types for the production and mutationOf of state
  • Reactive app architecture as a driver of app state
  • Android insets and IME (keyboard) behavior as state
  • Android permissions as state
  • Tiling for incremental loading (pagination) as state
  • Trees for representing app navigation as state
  • Jetpack Compose for stateful motionally intelligent global UI

๐Ÿšจโš ๏ธ๐Ÿšง๐Ÿ‘ท๐Ÿฟโ€โ™‚๏ธ๐Ÿ—๏ธ๐Ÿ› ๏ธ๐Ÿšจ

I try to keep the code at a near production quality, but this often takes a back seat to convenience and whim.

Again, the work presented here are the experiments of an immutable state and functional reactive programming zealot. It's far from objective, caveat emptor.

Architecture

Data layer

Offline-first

The app is a subscriber in a pub-sub liaison with the server. There is no pull to refresh, instead the app pulls diffs of ChangeListItem when the server notifies the app of changes made.

The following rules are applied to the data layer:

  • DAOs are internal to the data layer
  • DAOs expose their data with reactive types (Flow)
  • Reads from the data layer NEVER error.
  • Writes to the data layer may error and the error is bubbled back up to the caller
  • The NetworkService is internal to the data layer

Pub sub implementation

Pub sub in the app is backed by a change list invalidation based system. Its premise is:

  • Each model table on the server will have a sibling table that has a row that tracks a unique id that identifies a CRUD update (change_list_id). This unique id must have natural ordering.
  • CRUD updates to any model will cause an update for the change_list_id (akin to a new commit in git).
  • The client will then hit an endpoint asking for changes since the last change_list_id it has, or its local HEAD. A changelist of model ids that have changed will then be sent (akin to a git fetch)
  • The clients will then chew on the change list incrementally, updating its local HEAD as each update is consumed (akin to applying the pulled commits).

Real time updates are implemented with websockets via socket.io. I intend to move the android client to FCM for efficiency reasons in the future.

Models

The app offers 3 main data types:

  • Archive: Content I've produced over the years: Articles, projects and talks.
  • User: The creator of the content shown. This is really just me.
  • SavedState: App saved state. This is navigation state, and screen sate of each navigation destination.

Domain Layer

The domain layer offers abstractions that consolidate common patterns and business logic across each future and its screen. There are two main types here:

  • NavStateHolder: Manages the app navigation state and interacts with the SavedStateRepository. It provides Navigation as state.
  • GlobalUiStateHolder: Manages configuration for the app and adapts it over different form factors and screen sizes. It provides app level UI as state.

UI Layer

State production

All screen level state holders are implemented with unidirectional data flow as a functional declaration.

X as state

Navigation as state

This app treats navigation as state, and as such, it is completely managed by business logic. The Navigation state is persisted in the data layer with the SavedStateRepository and exposed to the app via the NavStateHolder.

Each destination in the app is represented by an AppRoute that exposes a single @Composable Render() function. The backing data structures for navigation are the tree like StackNav and MultiStackNav immutable classes. The root of the app is a MultiStackNav and navigation is controlled by a NavStateHolder defined as:

typealias NavStateHolder = ActionStateMutator<Mutation<MultiStackNav>, StateFlow<MultiStackNav>>

Global UI as state

The app utilizes a single bottom nav, toolbar and a shared global UI state as defined by the UiState class. This is what allows for the app to have responsive navigation while accounting for visual semantic differences between Android and desktop. Android for example uses the WindowManager API to drive it's responsiveness whereas desktop just watches it's Window size. The definition for the GlobalUiStateHolder is:

typealias GlobalUiStateHolder = ActionStateMutator<Mutation<UiState>, StateFlow<UiState>>

Paging as state

Pagination is implemented as a function of the current page and number of columns in the grid:

[out of bounds]                    -> Evict from memory
                                                   _
[currentPage - gridSize - gridSize]                 |
...                                                 | -> Keep pages in memory, but don't observe
[currentPage - gridSize - 1]   _                   _|                        
[currentPage - gridSize]        |
...                             |
[currentPage - 1]               |
[currentPage]                   |  -> Observe pages     
[currentPage + 1]               |
...                             |
[currentPage + gridSize]       _|                  _
[currentPage + gridSize + 1]                        |
...                                                 | -> Keep pages in memory, but don't observe
[currentPage + gridSize + 1 + gridSize]            _|

[out of bounds]                    -> Evict from memory

As the user scrolls, currentPage changes and new pages are observed to keep the UI relevant.

State restoration and process death

All types that need to be restored after process death implement the ByteSerializable interface. This allows them to de serialized compactly into a ByteArray which can then be saved to disk with a DataStore instance. The bytes are read or written with a type called the ByteSerializer.

Things restored after process death currently include:

  • App navigation
  • The state of each AppRoute at the time of process death

Lifecycles and component scoping

Screen state holders are scoped to the navigation state. When a route is removed from the navigation state, it's state holder has it's CoroutineScope cancelled:

appScope.launch {
            navStateStream
                .map { it.mainNav }
                .removedRoutes()
                .collect { removedRoutes ->
                    removedRoutes.forEach { route ->
                        println("Cleared ${route::class.simpleName}")
                        val holder = routeStateHolderCache.remove(route)
                        holder?.scope?.cancel()
                    }
                }
        }

Lifecycles aware state collection is done with a custom collectAsStateWithLifecycle backed by the following lifecycle definition:

data class Lifecycle(
    val isInForeground: Boolean = true,
)

Running

As this is a multiplatform app, syntax highlighting may be broken in Android studio. You may fare better building with Intellij.

Desktop: ./gradlew :desktop:run Android: ./gradlew :android:assembleDebug or run the Android target in Android Studio

License

Copyright 2021 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

me's People

Contributors

tunjid avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

me's Issues

`dropTarget` on a `Row` does not work after switching monitor

Environment

  • JetBrains Compose org.jetbrains.compose 1.1.1
  • Microsoft Windows 10.0.22616
  • Two monitors with different density.

Steps to Reproduce

  1. Add dropParent modifier on a surface.
  2. Create a row in this surface, adding dropTarget modifier.
  3. Run application. Then drag and drop function only works before I move the window into another monitor.

However, if I add dropTarget to TextField, it works correctly.

Example code

https://gist.github.com/KiruyaMomochi/04585a662b0167529c6bd03fbc3794fc

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.