Android Programjaim

Android, Flutter/Dart, Kotlin, Java, Unity, HarmonyOS

Flutter, dart es BloC architecture

2020. január 02. 13:10 - lacas8282

A Bloc/FlutterBloc-rol lesz szo a cikkben. https://github.com/felangel/bloc

Tehat ez is egy nepszerubb state management konyvtar, mint a MobX, csak maskepp mukodik. A mostani kodjaimat pl igy irom.

A lenyege a libnek, hogy megprobalja elkuloniteni az uzleti logikat a prezentacios retegtol.
Miert jo ez nekunk? A kod tisztabb lesz, szebb, amellett tesztelhetobb, ujrafelhasznalhatobb, attekinthetobb.

Egy par examplet latunk Felix Angelov repojaban is, ha akarjuk ezt is kovethetjuk alapkent. Bar en mar talalkoztam olyan sample koddal a repobol, ami nem mukodott.

Altalaban azt kell mondani, hogy nagyon hasonlo ez a cucc, mint a React Native-os REDUX. Persze en nem szeretem sajnos az RN-t, meg Reduxot se nagyon. Anno volt par honap szenvedesem vele.

Reakt Nativ sztrimeket (bocs a magyarert, dehat mit irjak? FOLYAMOT? azis gaz:/) hasznal ez a lib. Nezzunk rogton egy konkret peldat. (aki akar olvasgathat angolul meg RX-rol: https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/)

Pubspec.yaml-be betoljuk a

flutter_bloc: ^3.0.0

-ot a dependencies-ek koze, majd GET

Hogyha return 0  van, akkor eljenzes, minden sikerult, nem akadt ossze semmi semmivel.

A main dart file-ba a main method-ba kell egy

void main() async {

   ...

 BlocSupervisor.delegate = SimpleBlocDelegate();


Ez siman egy listener igazabol, ami a SimpleBlocDelegate-be kerul:

import 'package:bloc/bloc.dart';

class SimpleBlocDelegate extends BlocDelegate {
@override
void onEvent(Bloc bloc, Object event) {
super.onEvent(bloc, event);
print('bloc: ${bloc.runtimeType}, event: $event');
}

@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('bloc: ${bloc.runtimeType}, transition: $transition');
}

@override
void onError(Bloc bloc, Object error, StackTrace stacktrace) {
super.onError(bloc, error, stacktrace);
print('bloc: ${bloc.runtimeType}, error: $error');
}
}

Ez mondjuk legyen egy bloc_delegate.dart file-ba, es a main-bol importoljuk be...

Mint lathato lesz a kesobbiekben ha tortenik valami (Egy esemenyt atadunk a bloc-unknak, majd egy state-et visszaadunk), akkor ez a kis csoda loggolni fogja LogCat-re/vagy Run terminalra.

Ez arra szolgal, amire kitalaltak: Loggolasra. Hogy lasd mikor mi tortenik a hatterben...

main.dart-unk tehat ezt tartalmazza:

void main() async {
WidgetsFlutterBinding.ensureInitialized();
BlocSupervisor.delegate = SimpleBlocDelegate();

await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]).then((_) {
runApp(MyApp());

});
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '...',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocProvider<CounterBloc>(
create: (context) => CounterBloc(),
child: CounterPage(),
),
);
}
}

runApp(MyApp());


lefuttatja az elso gyoker widgetunket, ami a MyApp class.
Ez egy StateLess Widget ugye, nincsenek benne allapotok, valtozok.

home: BlocProvider

itt tortenik a varazslat. Egy providerrel megmondjuk, hogy melyik Bloc osztalyt fogjuk hasznalni melyik Page-nel.
Jo dolog, ha az elnevezesek kb tok ugyanazok, a kesobbiekben majd meglatjuk miert.

Csinalnunk kell counter.dart filet amiben a CounterPage lesz, es egy counter_bloc.dart -ot, amiben meg nyilvan a Bloc-unk.

A kodunk nem alap dolgokat fog hasznalni, mint abban amit Felix hasznal, de annyira nem is bonyolultat, hogy ne ertsuk mirol van szo. Alapbol a counteres peldanal maradunk, de lesz incremet/decrement event-unk (metodusunk), es lesz egy state-unk amit visszakapunk.

counter_bloc.dart

import 'package:bloc/bloc.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
@override
CounterState get initialState => CounterState(counter: 0);

@override
Stream<CounterState> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.decrement:
//await Future<void>.delayed(Duration(seconds: 1));
yield CounterState(counter: state.counter - 1);
break;
case CounterEvent.increment:
//await Future<void>.delayed(Duration(milliseconds: 500));
yield CounterState(counter: state.counter + 1);
break;
default:
throw Exception('unhandled event: $event');
}
}
}

 

Alapbol minden bloc felepitese hasonlo, extendalja a Bloc class-t, ami generikus cucc, van egy Event, State parosa. Annyi, hogy ez kb 'barmi' lehet, celszeru mar eleinte is class-okat rakni ra, State es Event elnevezesekkel, hogy tudjuk mit csinalunk.

Mit csinal a Bloc kb? Van egy magikus dobozod, amibe beledobsz egy eventet (mit akarsz csinalni) es ami ebbol kikop egy State-et (allapotot, valtozot, eredmenyt), amit megkapunk UI oldalon, igy nem kell szopni a setState-el es azzal, hogy mindig minden ujrabuildelodik/rajzolodik egy state change-kor.
Mindig van egy initialState ugye, ami az elso allapota a bemenetunknek, igy itt egy 0. (counter valtozo = 0)
Lesz egy CounterEvent es CounterState osztalyunk. Jelen esetben a CounterState egy enum, de en inkabb class-okat ajanlok erre is kesobb. Foleg ha sok Event es State lesz majd egy Bloc-hoz, akkor ajanlatos lesz egy BaseEvent es BaseState class, amibol oroklodik minden mas...

 Stream<CounterState> mapEventToState(CounterEvent event) async* {

 

Ez mi a franc? Kerdezhetnenk.

Pont az, amit magyarul ha leforditanank, ugyanaz tortenik. Mappeli a beerkezo Event-et egy kimeno State-e, miutan csinaltunk valamint a bemenettel. Mi az az async*? Hat az asyncrol mar biztos hallottal, Stream-eknel mikor yield-el adunk vissza erteket es nem return-al szokas hasznalni.
A peldakodbol kikommenteltem az await Future-okot, amik itt kb csak a webes tartalom eleresenek idejet korlatoznak, ha lenne.. (szemleltetnek egy kesest, amikor pl fetch-elunk egy adatot szerverrol)
Tehat varakozas siman.

CounterState ezek utan a counter valtozot vagy noveli vagy csokkenti es egy osztalyban visszaadja az UI-nak.

Nezzuk az UI-t:

class CounterPage extends StatelessWidget {

@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
BlocBuilder<CounterBloc, CounterState>(builder: (context, state) {
log("state ${state.counter}");
return Center(
child: Text(
'${state.counter}',
style: TextStyle(fontSize: 40),
),
);
}),
FloatingActionButton(
onPressed: () => BlocProvider.of<CounterBloc>(context).add(CounterEvent.decrement),
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
FloatingActionButton(
onPressed: () => BlocProvider.of<CounterBloc>(context).add(CounterEvent.increment),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
],
),
),
);
}

 

A MobX-os peldank lemasolva, csak BloC-al. Van 2 gombunk, egy textunk, meg egy
StatelessWidgetunk. 

Mi az ami uj?

Van egy BlocBuilderunk, itt ha a State valtozott az Event hatasara rebuildelodik (igy nem fogja az egesz UI-t refreshelni, csak azt ami itt benne van...)

BlocBuilder<CounterBloc, CounterState>(builder: (context, state) {
log("state ${state.counter}");
return Center(
child: Text(
'${state.counter}',
style: TextStyle(fontSize: 40),
),
);
}),

A builder kimenete maga az uj state.

Ha a State valtozik, tehat kiirunk LogCat-re egy Log state counter-t, ami vagy novelt vagy csokkentett ertek, es az UI ezen resze ujrabuildel.

FloatingActionButton(
onPressed: () => BlocProvider.of<CounterBloc>(context).add(CounterEvent.decrement),
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),


Es vegul: van egy FAV-unk, amire rabokve a BlocProvider-en keresztul elerheto CounterBloc -hoz hozzaadjuk a decrement vagy increment esemenyt (funkciot), ami tenylegesen noveli/csokkenti az erteket, majd visszadob egy CounterState new value-t, ami a counter erteke. Megjegyzes (BlocProvider.of<CounterBloc>(context) -et kirakhatjuk valtozoba is)

Vegul meg jojjon a State/Event classok/vagy enum-ok

enum CounterEvent { increment, decrement }

class CounterState {
final int counter;
final other;

CounterState({
this.counter = 0,
this.other = true,
});

@override
List<Object> get props => [
counter,
other,
];
}

 

Enumkent 2 funkcio, amit CSINALNI akarunk. Es maga a State, amit esetleg visszaadunk. Itt csinaltam egy other-t is, igy tudunk esetleg tobb dolgot atadni. Van aki egy State-et hasznal, van aki tobbet, van aki enumokat, van aki class-okat az eventekre.

En BaseEvent es BaseState classokat jelenleg, de tobbfele megoldas is lehet ra.

Jojjon az ertekeles:

POZITIVUM:
- state management
- nem frissul az egesz UI, mint setState-nel, hanem csak a Builder resz
- tisztabb kod
- elkulonult logika (uzleti es prezentacios reteg)
- nincs build minden egyes valtoztataskor, mint MobX-nel

NEGATIVUM:
- szenakazal meg igyis szulethet
- a vegen sok Event es State class-unk is lehet, ami mondjuk orokli az elozo Base osztalyt, es ez se biztos, hogy tul elegans megoldas (vegul (state is SomeState) -al vizsgalat)

Nekem tetszik jelenleg, ezt a megoldast hasznalom.

komment
süti beállítások módosítása