Как стать автором
Обновить

Комментарии 16

подскажите, а Kotlin Multipaltform насколько перспективный?
Есть же Flutter, зачем Kotlin Multipaltform?
Ну это примерно как «зачем C#, есть же Qt».

Kotlin Multipaltform — это просто «подмножество» (не совсем корректно, но лучше не придумал) языка, оптимизированное под нативную сборку на несколько платформ из одной кодовой базы.

Flutter — это фреймворк, довольно жёстко завязанный на собственнуй систему UI-компонентов.

Они кстати не сказать, что взаимоисключают друг друга. Некоторые знающие толк в разработке мсье умудряются делать бизнесс-логику на Kotlin, а слой UI на Flutter в одном проекте.

Спасибо за вопрос! Я позволю себе ответить вопросом на вопрос. Зачем нужен Flutter с его "другим" Dart, если теперь есть Kotlin Multiplatform? На мой взгляд у Flutter было два преимущества: возможность программировать сразу под несколько платформ и декларативный UI. Первый пункт больше не является преимуществом, и даже теперь имеет минусы в виде "другого" Dart. Второй пункт пока ещё актуален, но я думаю мультиплатформенный Compose будет и тогда в этом вопросе будет поставлена точка.


У KMP же есть следующие преимущества:


  • поддерживается больше платформ, среди которых есть нативные Linux x64, ARM и MIPS, watchOS, tvOS, Android Native, Windows и WASM. Полный список приведён здесь;
  • есть прямой доступ к API платформ с помощью expect/actual
  • можно легко управлять тем, какой код делать общим, а какой делегировать в платформы.
  • нативный UI (Android Views, Jetpack Compose, UIKit, SwiftUI и т.д.) позволяет поддерживать особенности платформ

В целом, KMP как инструмент мне кажется на много мощнее.

получается, что Flutter может оказаться бесполезным в будущем, если Kotlin Multiplatform будет развиваться; правильно понимаю?
В стратегическом плане лучше изучать Kotlin Multiplatform вместо Flutter?

На мой взгляд именно так. Могу дополнительно привести ссылку на твит одного известного в Андроид разработке человека. :-)

Спасибо, очень интересно. Жду продолжения

Спасибо, что прочитали!

Просто и доступно. Спасибо!

Зачем все эти сложности с Intent, да еще в сторе внутренний DSL из Effect? Почему нельзя сделать просто, например так
//Stream of model state changes. Stateful view or controller subscribes to updates, and 
//convert state changes into view/subview updates.
public IObservable<StateChange> Updates { get; }

public async Task Reload()
{
    if(IsLoading) return;
    try
    {
        IsLoading = true;
        var netData = await network.LoadAsync();
        Images = await parser.Parse(netData);
    }
    catch (Exception e)
    {
         ImageLoadError = new Error(e);
    }
    finally 
    {
        IsLoading = false;
    }
}

[State]
public bool IsLoading { get; private set; }

[State]
public ImageData Images {get; private set; }
[State]
public Error ImageLoadError {get; private set; }



Я могу представить, что сложная модель на сервере может использовать Effect, чтобы выполнять IO пачкой. Есть входная очередь запросов в модель, на выходе очередь из Effect, и обработчик эффектов, который их пачками складываем в базу, рассылает по веб-сокетам и т.п. Но зачем все эти сложности в мобильном приложении мне не понятно.

Спасибо за вопрос! В вашем примере Представление/контроллер — stateful, однако в MVI они должны быть stateless. Единым источником правды должна быть модель (Store). Таким образом Ваш вопрос сводится к "Зачем нужен MVI?".
Это уже особо не относится к теме этой статьи, но я попробую ответить.
Единый источник правды делает состояние всегда консистентным, что существенно упрощает поддержку и отладку когда. Также это даёт возможность делать крутые штуки вроде time travel отладки. Разделение на такие сущности, как Intent, State, Model и Event, позволяет делать представление и модель независимыми друг от друга. Опять же, компоненты, между которыми отсутствует связность, легче поддерживать и тестировать.


Спасибо, что прочитали статью!

В приведённом мной примере контроллера может и не быть, может просто view с методом Render(StateChange). Все компоненты независимы, модель источник правды. Time travel нет, но он не стоит этих сложностей. В реальной жизни есть stateful widgets, которые с time travel не дружат.


Собственно да, мой вопрос сводится к тому, что нам даёт заворачивание запросов к модели в Intent по сравнению с вызовом методов. Model.Dispatch(new Intent(DoSomething)) против простого Model.DoSomething()

Если у Представления есть метод render(StateChange) то по определению Представление зависит от от Состояния Модели, что делает Представление и Модель связанными. Введение отдельной Модели Представления избавляет от этой связности. Тоже самое в обратную сторону, судя по всему предполагается, что Представление будет обращаться к Модели на прямую (вызывать метод Reload). Это прямая зависимость Представления от Модели.


Далее, состояние в вашем случае представлено набором отдельных изменяемых переменных. Если их начать изменять из разных мест (не только из функции Reload), то придётся всё синхронизировать. А это потенациальная возможность получить гонки и неконсистентные состояния (например, флаг прогресса загрузки стоит, а загрузка не идёт).


MVI решает эти проблемы, представляя входы и выходы как потоки данных, которые можно преобразовывать. Имея класс Event можно преобразоывать его в Intent направить на вход Модели. Или же направить в некий AnalyticsTracker для аналитики. Можно преобразовать в Output Контроллера и выставить этот поток наружу.


В целом за счёт небольшого количества "boilerplate" кода достигается меньшая связность, поддерживаемость и безопасность.

Если у Представления есть метод render(StateChange) то по определению Представление зависит от от Состояния Модели, что делает Представление и Модель связанными....


И что с того? Это же UI, модель делается для того, чтобы ее удобно было отображать. Задача модели, буквально, загрузить данные и представить их в виде, удобном для отображения. Вся цепочка, снизу вверх выглядит вот так
1. Сервисы, которые тянут данные из хранилища. Либо REST, WebSocket, Local File, DB, etc. Они работают с данными заточенными под удобное хранение и извлечение.
2. Модель для UI, которая транслирует данные их сервисов в вид удобный для отображения. Также содержит transient state, статус загрузки.
2.5 Опциональный компонент, который рендерит модель, использую методы view. Это если мы хотим, чтобы view не знала о модели. Тогда из view торчит API в терминах Color, Font, Border, Text и т.п. А компонент отображает изменения модели на это API.
3. View, который эту модель отображает, самостоятельно или с посредством помощника как описано выше.

Я не знаю ни одной причины иметь другую схему, кроме желания следовать каким-то там принципам. Если мы с этим согласимся, то связь между view и model становится желательной. Возможно они не имеют ссылок друг на друга, но их логика и структура согласованы — меняется одно, скорее всего меняется и другое. Чтобы избавится от явной ссылки есть контроллер, вы пишите «view.events.map(Event::toIntent).subscribeScoped(onNext = store::onNext)», я напишу «View.Event += () => Model.Reload()». Никакой разницы, кроме того, что у вас явно очереди прописаны, а у меня они неявно присутствуют.

Далее, состояние в вашем случае представлено набором отдельных изменяемых переменных. Если их начать изменять из разных мест (не только из функции Reload), то придётся всё синхронизировать. А это потенациальная возможность получить гонки и неконсистентные состояния (например, флаг прогресса загрузки стоит, а загрузка не идёт).


Про потоки и синхронизацию нужно думать в любом случае, поэтому у вас есть «observeOn(mainScheduler)», без него все развалится. Работу с моделью вам нужно либо явно синхронизировать, либо делать ее в выделенном потоке. А это значит, что когда parser закончил работу он должен следующее действие делать в потоке модели. А если вдруг об этом кто-то забыл, то будет беда.

>Имея класс Event можно преобразоывать его в Intent направить на вход Модели. Или же направить в некий AnalyticsTracker для аналитики.

Я напишу «View.Event += e => Analytiсs.RecordEvent(e.ToAnalyticalEvent())» и не буду морочить себе голову высокими материями.

>В целом за счёт небольшого количества «boilerplate» кода достигается меньшая связность, поддерживаемость и безопасность.

1. Связность, осталась такой же, описал выше.
2. Поддерживаемость — стала хуже, больше кода, больше концепций, больше краевых случаев
3. Безопасность — осталась такой же, потоки все еще проблема.

Я знаю один кейс, кода ваш подход работает, я даже именно так модель и пишу, но на сервере. Модель сложная, сидит в памяти и нужно ее очень быстро обрабатывать. На входе в модель очередь инструкций (ваши Intent), на выходе очередь «изменений модели» (ваши Model). Выходная очередь позволяет изменения модели паковать в пачки и эффективно делать I/O в БД и сеть. Но дело не только в очередях — в такой модели машина состояний нужна в явном виде, потому что сервер может упасть и нужно будет с произвольной точки восстанавливаться.

Похоже, обсуждение переходит в обсуждение полезности шаблона MVI, что выходит за рамки данной статьи. Каждая команда выбирает свой подход к разработке. Мы последовательным путём пришли именно к MVI. Мы пробовали разные подходы и остановились именно на этом. У нас есть необходимость переиспользовать код в разных проектах. Мы иногда имеем более одной реализации вью. Иногда имеем более одно реализации бизнес логики. От инкрементального обновления вью мы ушли впользу stateless. От размызывания состояния по переменным мы ушли в пользу единого неизменяемого "хранилища" и обновления из одного места — редуктора. Кроме того, жизненный цикл вью может быть уже чем у модели. Мы можем отсоеденить вью от модели и потом присоеденить новый экземпляр, может быть даже другую реализацию.

Вообще интересный подход к разработке.
Если назвать KittenComponent — KittenPresenter, всё встаёт на свои места. По сути это тот же MVP, только state храниться в моделях, а Presenter уже больше proxy.
Из плюсов, явно, это восстановление состояния после пересоздания Fragment, не нужно никаких дополнительных фреймворков.
Хотел бы увидеть большой пример, не будет ли это всё громоздко.

Спасибо, что прочитали! Большого примера в открытом виде, к сожалению, нет. Но общий принцип — делить на модули и не допускать их распухания. Компонент (презентер) — необязательно экран, это может быть и его часть. Общение между ними можно наладить при помощи входов/выходов, по такому же принципу, как Store соединяется со View внутри модуля.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий