Code Monkey home page Code Monkey logo

uimodel's Introduction

UiModel, a lean and clean state managnment for Flutter Apps.

Package uimodel helps to build comprehensible Models and ViewModels for Flutter apps, then have it linked with UI View layer in a single line per your custom Widget.

Tests coverage: 100.0% (77 of 77 lines)

[UiModel] mixin is a thin wrapper around Toggler, an (observable) state machine register that runs underhood your Models. It exposes a subset of Toggler api useful in Widget build method.

[UiModelLink] mixin adds the magic watches(model, changes-mask) link layer to the Widget.

[UiNotifier] is a concrete implementation of a ToggledNotifier that talks directly to Flutter [Element]s of your UI Widgets.

Together these allow a "StatelessWidget with UiModelLink" to observe changes in "Model with UiModel", and rebuild accordingly. All by a single watches invocation in the Widget's build:

// in mymodels.dart:
class ViewModel with UiModel {...} // below seen as 'm' singleton

// in mywidget.dart:
class MyWidget extends StatelessWidget with UiModelLink {
  MyWidget({ Key? key, }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    watches(m, smDn | smUp | smEnd ); // three flags/events in 'm' observed
    // watches(m.viewmodel, smPos | smAlt | smEnd ); // or in m.submodel

Example App with state management based on Toggler then linked to Flutter View with [UiModelLink] can be found in the example/main.dart file. Both UI and Model code is also given at bottom of this README.

Toggler based ViewModels, a HOWTO

  • TODO

Example App:

example app main page

UI code:

import 'package:flutter/material.dart';
import 'package:uimodel/uimodel.dart'; // UiModel, UiModelLink
import 'package:toggler/toggler.dart'; // for DebugInfoBar UI and our Model

final m = ViewModel(); // "ambient" singleton instance

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Toggler and UiModel Example',
      theme: ThemeData(
        primarySwatch: Colors.lime,
      ),
      home: const Scaffold(
        body: CounterView(),
      ),
    );
  }
}

/// main route page
class CounterView extends StatelessWidget {
  const CounterView({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      Counter(),
      DebugInfoBar(),
      const OperatorBar(),
      CountryBar(),
    ]);
  }
}

View is wired to its ViewModel notifier with masks (prefixed sm) of a Toggler index (prefixed tg). Both are const int numbers. Indice are used to read and set flags state, masks are used to select more than one flag to observe. You may generate index/mask pairs using script that comes with Toggler package.

const tgUp = 10; // tgIndex (of tgUp change signal)
const smUp = 1 << tgUp; // smMask (of above tgIndex)
const tgDn = 11;
const smDn = 1 << tgDn; // ...more definitions ommited here

/// link Counter to UiModel (here explicitly via UiModelLink mixin. (In Widgets
/// below we will use `extends UiModeledWidget` shim, it means the same).
class Counter extends StatelessWidget with UiModelLink {
  Counter({
    Key? key,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    watches(m, smDn | smUp | smInfo); // three flags/signals in 'm' observed
    // watches(m.submodel, smSubSmth | smSubOther); // here: two of submodel
    return Expanded(
        child: Center(
      child: ListView(shrinkWrap: true, children: [
        Column(
          children: [
            Text(
              'Counted ${m.ctTxt}', // read straight from Model
              style: Theme.of(context).textTheme.headline4,
            ),
            m[tgInfo] // if tgInfo is set, display 'label' with debug info
                ? Text(m.label)
                : const Text(
                    'skipping unlucky numbers\nand multiplies of 15',
                    textAlign: TextAlign.center,
                  ),
          ],
        )
      ]),
    ));
  }
}

/// UiModeledWidget is a sugar shim for `StatelessWidget with UiModelLink`
class CountryBar extends UiModeledWidget {
  CountryBar({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    watches(m, smNoCntry - 1); // observe all in radioGroup
    return Container(
      constraints: const BoxConstraints.expand(height: 36),
      child: m[tgNoCntry]
          ? ListView(
              shrinkWrap: true,
              scrollDirection: Axis.horizontal,
              children: [
                  Row(
                    children: countryid // make buttons for all in radioGroup
                        .map((e) => TextButton(
                            style: bBlack,
                            onPressed: () => m.cntry(e),
                            child: Text(countries[e])))
                        .toList(),
                  )
                ])
          : ElevatedButton(
              onPressed: () => m.cntry(tgNoCntry), child: Text(m.country)),
    );
  }
}

class DebugInfoBar extends UiModeledWidget {
  DebugInfoBar({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    watches(m, smAny); // any changes
    final ntf = (m.tg.notifier as UiNotifier);
    final info = m[tgInfoTB] // either bin or hex, on a tap
        ? 'chb bin: ${b(m.tg.chb)}\nserial:${m.tg.serial} dbg:${m.dbg}'
        : 'chb hex: ${h(m.tg.chb)}, serial:${m.tg.serial}\nNotifiers: ${ntf.observers} recent:${m.tg.recent}';
    return m[tgInfo]
        ? ButtonBar(
            alignment: MainAxisAlignment.start,
            children: [
              TextButton(
                style: bBlue,
                // E[tgIndex] is true if tgIndex is enabled.
                // Example model logic disables InfoBar taps for India.
                onPressed: m.E[tgInfoTB] ? () => m.toggle(tgInfoTB) : null,
                child: Text(info),
              )
            ],
          )
        : const SizedBox.shrink();
  }
}

/// No rebuilds needed, hence no ModelLink: just wire up buttons to Model
class OperatorBar extends StatelessWidget {
  const OperatorBar({
    Key? key,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ButtonBar(children: [
      IconButton(onPressed: m.info, icon: const Icon(Icons.info_outline)),
      IconButton(onPressed: m.zero, icon: const Icon(Icons.first_page)),
      IconButton(onPressed: m.sub, icon: const Icon(Icons.remove_circle)),
      IconButton(onPressed: m.add, icon: const Icon(Icons.add_circle)),
      IconButton(onPressed: m.max, icon: const Icon(Icons.last_page)),
    ]);
  }
} // end of App UI

Model code:

Example class below is our whole App Model, with some bits being (Model) signals, and some being (ViewModel) state that affects Widgets final look:

class ViewModel with UiModel {
  int _ct = 0;
  int _sk = 0;
  int dbg = 0; // shown in info lines
  void _mark(int i) => tg.toggle(i);
  String country = countries[tgCtSel];
  int skipnum = -1; // "unlucky" number to skip
  ViewModel({ToggledNotifier? notifier}) {
    tg.radioGroup(tgChina, tgNoCntry); // tg from UiModel mixin
    tg.set1(tgCtSel); // init its state in constructor
    // ... set/restore state here, then:
    tg.fix = fix; // bind your business / ViewModel logic
    tg.notifier = notifier ?? UiNotifier(); // last! attach notifier
  }
  String get label => tg[tgUpSkip] || tg[tgDnSkip]
      ? 'skipped: $_sk (${tg.serial})'
      : tg.recent == tgUp
          ? 'went up (${tg.serial})'
          : tg.recent == tgDn
              ? 'went down (${tg.serial})'
              : '(state changes count ${tg.serial})';
  String get ctTxt => '$_ct';
  int get ct => _ct;
  set ct(int v) {
    if (_ct == v) return;
    _sk = _ct;
    _ct = v;
    if (_ct > counterMax) _ct = 0;
    if (_ct < 0) _ct = counterMax;
    _mark(_ct < _sk ? tgDn : tgUp);
  }

  // Do not use closures in UI event bindings. Model methods allows for
  // easy testing and can be later refactored to add functionality
  // without even touching View layer code.
  void info() => tg.toggle(tgInfo);
  void cntry(int i) => tg.set1(i);
  void zero() => ct = 0;
  void max() => ct = counterMax;
  void sub() => ct--;
  void add() => ct++;
  // end of ViewModel part

  /// Our "business logic": counter skips unlucky numbers as recognized
  /// in user set country, and also always skips multiplies of '15'.
  bool fix(Toggler o, Toggler n) {
    skipUp() {
      _sk = _ct;
      _ct++;
      n.set1(tgUpSkip);
    }

    skipDn() {
      _sk = _ct;
      _ct--;
      n.set1(tgDnSkip);
    }

    n.clear(tgDnSkip);
    n.clear(tgUpSkip);
    if (n.recent > tgNoCntry && n[tgNoCntry]) {
      n.set1(tgCtSel);
    }
    if (n.recent < tgNoCntry) {
      skipnum = unlucknum[n.recent]; // set unlucky number
      country = countries[n.recent]; // and label
      n.recent == tgIndia ? n.disable(tgInfoTB) : n.enable(tgInfoTB);
    } else if (n.recent == tgUp) {
      if (_ct == skipnum) skipUp();
      if (_ct != 0 && _ct % 15 == 0) {
        _sk = _ct;
        _ct += 1;
        if (_ct == skipnum) skipUp();
        n.set1(tgUpSkip);
      }
    } else if (n.recent == tgDn) {
      if (_ct == skipnum) skipDn();
      if (_ct != 0 && _ct % 15 == 0) {
        _sk = _ct;
        _ct -= 1;
        if (_ct == skipnum) skipDn();
        n.set1(tgDnSkip);
      }
    }
    return true;
  } // end of Model part
}

FAQ

  • Q. How much ballast this adds to my App?

  • A. Both [uimodel] and toggler packages are lean, Toggler having zero dependencies, and Uimodel depending only on Toggler and flutter/widgets Element. Toggler instance adds 64 bytes to the [Object] (then to your Model via [UiModel] mixin). [UiModelLink] mixin adds not much more to the Widget. The per Model [UiNotifier] object costs a dart:core Map then adds a dart:core List per every mask watched. Both [toggler] and [uimodel] libraries together add less than 255 loc to your executable.

  • Q. Looks too good to be true. Where is the trap?

  • A. You must understand you work with indice and bitmasks. These are named for reading humans but compiler sees only numbers. Analyzer will not tell you "hey, its a wrong type here". Please read about caveats in Toggler documentation. Then adhere to proposed naming convention.

uimodel's People

Contributors

ohir avatar

Watchers

 avatar

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.