This content originally appeared on Level Up Coding - Medium and was authored by Sk Ahron
Server-Driven UI in Flutter from Scratch: Part 5 — Building Complete App End-to-End using Serverpod
In this Article, We will be implementing the server driven UI app backend using Serverpod which is dart based framework to build backend.
We will be building Pokedyn flutter app which uses serve_dynamic_ui package and Serverpod for backend.
https://www.youtube.com/watch?v=rC6ibQc4KpA
This article will be more inclined towards designing UI for your server driven UI app. This article is just to give you an idea how you can build app server which serves dynamic UI for your app.
Note: This is not the most efficient way to implement your backend it is just to give you an idea.
Choosing Serverpod to develop our backend as it is dart based implementation to develop backends so do it checkout.
Lets Begin.
You might have got the idea what we are going to build. We will create a simple app which gives the Pokemon Details with basic authentication and good looking UI using serve_dynamic_ui.
Note: You must read previous parts as I will not cover what is already covered previously
Checkout Pokedyn app code
Let Start Implementing
As we are using serverpod and serverpod has a starter repo
you need to follow commands mentioned by serverpod
dart pub global activate serverpod_cli
serverpod create pokedyn
it will create starter code having main folders pokedyn_flutter, pokedyn_server and pokedyn_client.
pokedyn_flutter: is the place our flutter app code will be there.
pokedyn_server: is the place where we write backend logic.
pokedyn_client: is the place where all the generated code will be created by serverpod.
So, first we will work in pokedyn_flutter folder.
this section will be mostly designing our UI by creating Json Structure which we want for each page.
- In pubspec.yaml add
serve_dynamic_ui: ^1.0.1
this will get the serve_dynamic_ui dependency so that we can build our dynamic screens app.
2. Now, we will first create dynamic screen jsons which later we will fetch from our server as per our page endpoints.
We will first create json for this screen our login screen
if you see form elements are on top of full screen image. I have used dy_stack where dy_image and dy_column are in the stack.
{
"type": "dy_scaffold",
"data": {
"key": "1236456",
"appBar": {
"pageTitle": "Pokedyn Login",
"appBarGradient": "-1.0,0.0;1.0,0.0;0xff42A5F5,0xffAB47BC"
},
"scrollable": false,
"mainAxisAlignment": "start",
"children": [{
"type": "dy_container",
"data": {
"key": "223236423",
"backgroundColor": "0xff90EE90",
"height": -1,
"child": {
"type": "dy_stack",
"data": {
"key": "2234656231",
"alignment": "center",
"children": [
{
"type": "dy_container",
"data": {
"key": "123123",
"child": {
"type": "dy_image",
"data": {
"key": "212226323233",
"src": "https://w0.peakpx.com/wallpaper/998/140/HD-wallpaper-pikachu-pokemon.jpg",
"width": -1,
"height": -1,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_login_placeholder.jpg",
"transitionDuration": ":::3:"
}
}
}
},
{
"type": "dy_column",
"data": {
"key": "12313",
"mainAxisAlignment": "end",
"children": [
{
"type": "dy_container",
"data": {
"key": "34755345345345",
"width": 300,
"child": {
"type": "dy_text_field",
"data": {
"key": "784867234hwdf",
"initialText": "",
"textFieldDecoration": {
"textAlign": "start",
"obscureText": false,
"maxLines": 1,
"decoration": {
"fillColor": "0xFFFFFFFF",
"filled": true,
"hintText": "Enter Email Address",
"labelText": "Email",
"focusBorderColor": "0xFFFFA500",
"borderWidth": 3,
"borderRadius": 10,
"labelStyle": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xFF000000"
},
"hintStyle": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xFF888888"
}
},
"style": {
"color": "0xFF000000",
"fontSize": 25
},
"errorText": "Please Enter Valid Email Address.",
"errorTextStyle": {
"fontWeight": "bold",
"color": "0xffFF0000"
},
"cursorColor": "0xFF0000FF"
},
"regexValidator": "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
}
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "827487246",
"height": 30
}
},
{
"type": "dy_container",
"data": {
"key": "98274924724",
"width": 300,
"child": {
"type": "dy_text_field",
"data": {
"key": "8723462434",
"initialText": "",
"textFieldDecoration": {
"textAlign": "start",
"obscureText": false,
"maxLines": 1,
"decoration": {
"fillColor": "0xFFFFFFFF",
"filled": true,
"hintText": "Enter User Name",
"labelText": "UserName",
"focusBorderColor": "0xFFFFA500",
"borderWidth": 3,
"borderRadius": 10,
"labelStyle": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xFF000000"
},
"hintStyle": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xFF888888"
}
},
"style": {
"color": "0xFF000000",
"fontSize": 25
},
"errorText": "User Name Should be At least 6 characters long.",
"errorTextStyle": {
"fontWeight": "bold",
"color": "0xffFF0000"
},
"cursorColor": "0xFF0000FF"
},
"regexValidator": "^.{6,}$"
}
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "82462842",
"height": 30
}
},
{
"type": "dy_button",
"data": {
"key": "87628624",
"buttonColor": "0xff90EE90",
"buttonBorderRadius": 10,
"padding": "10",
"width": 200,
"child": {
"type": "dy_text",
"data": {
"key": "23242342",
"text": "Login",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
},
"action": {
"actionString": "/login"
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "82462842",
"height": 150
}
}
]
}
}
]
}
}
}
}]
}
}
if you see, I am using dy_image where width and height of the image set is screen size by passing -1. if you pass negative value to height and width internally serve_dynamic_ui will consider it as full screen height and width not including app bar and status bar height. If you see I have also added transitionDuration which will give that smooth transition between placeholder image and network image.
{
"type": "dy_image",
"data": {
"key": "212226323233",
"src": "https://w0.peakpx.com/wallpaper/998/140/HD-wallpaper-pikachu-pokemon.jpg",
"width": -1,
"height": -1,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_login_placeholder.jpg"
}
}
Then In the dy_column we are using 2 dy_text_field widgets and a button
{
"type": "dy_column",
"data": {
"key": "12313",
"mainAxisAlignment": "end",
"children": [
{
"type": "dy_sized_box",
"data": {
"key": "82462842",
"height": 30
}
},
{
"type": "dy_container",
"data": {
"key": "34755345345345",
"width": 300,
"child": {
"type": "dy_text_field",
"data": {
"key": "784867234hwdf",
"initialText": "",
"textFieldDecoration": {
"textAlign": "start",
"obscureText": false,
"maxLines": 1,
"decoration": {
"fillColor": "0xFFFFFFFF",
"filled": true,
"hintText": "Enter Email Address",
"labelText": "Email",
"focusBorderColor": "0xFFFFA500",
"borderWidth": 3,
"borderRadius": 10,
"labelStyle": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xFF000000"
},
"hintStyle": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xFF888888"
}
},
"style": {
"color": "0xFF000000",
"fontSize": 25
},
"errorText": "Please Enter Valid Email Address.",
"errorTextStyle": {
"fontWeight": "bold",
"color": "0xffFF0000"
},
"cursorColor": "0xFF0000FF"
},
"regexValidator": "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
}
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "827487246",
"height": 30
}
},
{
"type": "dy_container",
"data": {
"key": "98274924724",
"width": 300,
"child": {
"type": "dy_text_field",
"data": {
"key": "8723462434",
"initialText": "",
"textFieldDecoration": {
"textAlign": "start",
"obscureText": false,
"maxLines": 1,
"decoration": {
"fillColor": "0xFFFFFFFF",
"filled": true,
"hintText": "Enter User Name",
"labelText": "UserName",
"focusBorderColor": "0xFFFFA500",
"borderWidth": 3,
"borderRadius": 10,
"labelStyle": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xFF000000"
},
"hintStyle": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xFF888888"
}
},
"style": {
"color": "0xFF000000",
"fontSize": 25
},
"errorText": "User Name Should be At least 6 characters long.",
"errorTextStyle": {
"fontWeight": "bold",
"color": "0xffFF0000"
},
"cursorColor": "0xFF0000FF"
},
"regexValidator": "^.{6,}$"
}
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "82462842",
"height": 30
}
},
{
"type": "dy_button",
"data": {
"key": "87628624",
"buttonColor": "0xff90EE90",
"buttonBorderRadius": 10,
"padding": "10",
"width": 200,
"child": {
"type": "dy_text",
"data": {
"key": "23242342",
"text": "Login",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
},
"action": {
"actionString": "/login"
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "82462842",
"height": 150
}
}
]
}
}
I have set regex validators for both text field widgets and the dy_button click action string is /login this is a custom action which we will create.
So, we will create the login action handler
class LoginActionHandler extends ActionHandler {
@override
void handleAction(BuildContext? context, Uri action,
Map<String, dynamic>? extras, OnHandledAction? onHandledAction) async {
if (context?.mounted ?? false) {
ActionHandlersRepo.handle(
ActionDTO("/form", extras), extras!['widget'], context!,
(value) async {
if (value is Map) {
SessionManagerState.instance.sessionStreamController.sink.add(
SessionOnAuthenticatedEvent(
authInfo: {
'email': value['textFieldData'][0],
'userName': value['textFieldData'][1],
},
),
);
}
});
}
}
}
here is the code for it as after filling the details user presses the button it will call this action handler. If you see internally in this action we are calling /form action which help use to get all the form values if valid otherwise it will show the error text which we passed.
For serve_dynamic_ui to detect this custom action handler we have to register this handler.
void main() {
WidgetsFlutterBinding.ensureInitialized();
ServeDynamicUI.init(actionHandlers: {
RegExp(r'(^/?login/?$)'): LoginActionHandler(),
});
runApp(const MyApp());
}
I am registering the LoginActionHandler in init of package make sure you add this line. If you don’t call this before init it will create issues in page caching.
WidgetsFlutterBinding.ensureInitialized();
Now, to test this what we have to do is in runApp add MyApp widget which has ServeDynamicUIMaterialApp and also make sure to pass navigationKey otherwise serve_dynamic_ui will not be able to detect context of application properly.
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late GlobalKey<NavigatorState> navigationKey;
@override
void initState() {
navigationKey = GlobalKey<NavigatorState>();
super.initState();
}
@override
Widget build(BuildContext context) {
return ServeDynamicUIMaterialApp(
navigatorKey: navigationKey,
home: (context) {
return ServeDynamicUI.fromAssets(
'assets/json/login_page.json',
);
},
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: false,
),
debugShowCheckedModeBanner: false,
);
}
}
and run the app now
if you see after adding the values in fields error goes away but we are on same screen behind the scenes in login handler we have transitioned from NoAuthenticated to Authenticated State but we are not using SessionManagerWidget which is specifically developed for Session Management. check out Part-4 article to know more about SessionManager.
SessionManagerWidget(
onUndetermined: () {
return ServeDynamicUI.fromAssets(
'assets/json/auth_loader.json',
);
},
onAuthenticated: () {
return ServeDynamicUI.fromNetwork(
DynamicRequest(
url:
'http://$localhost:8080/pokedynHomepage/getPage?isPageCacheEnabled=true',
requestType: RequestType.get,
sendTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
),
templateJsonPath: 'assets/json/shimmers/pokedyn_home_page_shimmer.json',
);
},
deAuthenticated: () {
return ServeDynamicUI.fromNetwork(
DynamicRequest(
url:
'http://$localhost:8080/authPage/getPage?isPageCacheEnabled=true',
requestType: RequestType.get,
sendTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
),
templateJsonPath: 'assets/json/logout_loader.json',
);
},
onAuthenticationInProgress: () {
return ServeDynamicUI.fromAssets(
'assets/json/loader.json',
);
},
deAuthenticationInProgress: () {
return ServeDynamicUI.fromAssets(
'assets/json/loader.json',
);
},
onAuthenticationExpired: () {
return const SizedBox();
},
notAuthenticated: () {
return ServeDynamicUI.fromNetwork(
DynamicRequest(
url:
'http://$localhost:8080/authPage/getPage?isPageCacheEnabled=true',
requestType: RequestType.get,
sendTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
),
templateJsonPath: 'assets/json/auth_loader.json',
);
},
)
above is the SessionManager Widget based on session state we will get widgets. As you can see above for different states we have different widget builders. Don’t worry if you are seeing network urls we will be adding them later.
So, Now we will design our home page.
This HomePage is paginated as we will have lot of pokemons. we cannot fetch all the pokemons data at once and render them all at once. So we will use pagination. Refer Part-4 for Pagination Implementation.
{
"type": "dy_scaffold",
"data": {
"key": "1236456",
"appBar": {
"pageTitle": "Pokedyn Home Page",
"appBarGradient": "-1.0,0.0;1.0,0.0;0xff42A5F5,0xffAB47BC",
"rightActions": [
{
"type": "dy_gesture_detector",
"data": {
"key": "23424234",
"onTapAction": {
"actionString": "/logOut"
},
"child": {
"type": "dy_image",
"data": {
"key": "24234234234",
"src": "assets/images/ic_logout.png",
"height": 30,
"width": 30,
"imageType": "asset",
"fit": "fill"
}
}
}
}
]
},
"scrollable": true,
"paginated": true,
"mainAxisAlignment": "start",
"padding": "20",
"itemsSpacing": 10,
"nextUrl": "assets/json/pokedyn_homepage?offset=5&limit=10.json",
"children": [
{
"type": "dy_gesture_detector",
"data": {
"key": "345435345",
"onTapAction": {
"actionString": "/moveToScreen",
"extras": {
"url": "assets/json/pokemon_detail_page?pokemon=1.json",
"urlType": "local",
"navigationType": "screen",
"navigationStyle": "push",
"loaderWidgetAssetPath": "assets/json/shimmers/pokedyn_detail_page_shimmer.json"
}
},
"child": {
"type": "dy_container",
"data": {
"key": "nTxG4llzAfgFnue",
"containerGradient": "-1.0,0.0;1.0,0.0;0xff42A5F5,0xffAB47BC",
"height": 350,
"showBorder": true,
"borderColor": "0xffffffff",
"borderRadius": 15,
"child": {
"type": "dy_column",
"data": {
"key": "zd6C8y3RLOiXTRr",
"children": [
{
"type": "dy_container",
"data": {
"key": "zYuTfvGJWBcXuW5",
"child": {
"type": "dy_image",
"data": {
"key": "zeNXHx8CCuj3dlu",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png",
"width": 250,
"height": 200,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
},
{
"type": "dy_text",
"data": {
"key": "a3jiu8s4mdk1IzK",
"text": "bulbasaur",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "rJgW6Zk6YbbAADk",
"height": 30
}
},
{
"type": "dy_container",
"data": {
"key": "Ae37DJYj3s0NT1C",
"backgroundColor": "0xff7AC74C",
"borderRadius": 20,
"width": 250,
"padding": "10",
"showBorder": true,
"child": {
"type": "dy_text",
"data": {
"key": "nAWlBajbvCQoaXQ",
"text": "Type: grass",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
}
}
}
]
}
}
}
}
}
},
{
"type": "dy_gesture_detector",
"data": {
"key": "456657657",
"onTapAction": {
"actionString": "/moveToScreen",
"extras": {
"url": "assets/json/pokemon_detail_page?pokemon=2.json",
"urlType": "local",
"navigationType": "screen",
"navigationStyle": "push"
}
},
"child": {
"type": "dy_container",
"data": {
"key": "jofXwHp6Eo7Dm8Y",
"containerGradient": "-1.0,0.0;1.0,0.0;0xff42A5F5,0xffAB47BC",
"height": 350,
"showBorder": true,
"borderColor": "0xffffffff",
"borderRadius": 15,
"child": {
"type": "dy_column",
"data": {
"key": "vft9aYbZQZbp56Y",
"children": [
{
"type": "dy_container",
"data": {
"key": "wrVTyPeSca0teFW",
"child": {
"type": "dy_image",
"data": {
"key": "CRkuwKwMrAjtwZZ",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/2.png",
"width": 250,
"height": 200,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
},
{
"type": "dy_text",
"data": {
"key": "B0yT0FmyBqlu4BF",
"text": "ivysaur",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "H6NbRjb8EWpdeY1",
"height": 30
}
},
{
"type": "dy_container",
"data": {
"key": "vlasgZgXgN3f0ZP",
"backgroundColor": "0xff7AC74C",
"borderRadius": 20,
"width": 250,
"padding": "10",
"showBorder": true,
"child": {
"type": "dy_text",
"data": {
"key": "ll75AYgtN9NLdlh",
"text": "Type: grass",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
}
}
}
]
}
}
}
}
}
},
{
"type": "dy_gesture_detector",
"data": {
"key": "242424234",
"onTapAction": {
"actionString": "/moveToScreen",
"extras": {
"url": "assets/json/pokemon_detail_page?pokemon=3.json",
"urlType": "local",
"navigationType": "screen",
"navigationStyle": "push"
}
},
"child": {
"type": "dy_container",
"data": {
"key": "MnspjcOMAiroUGQ",
"containerGradient": "-1.0,0.0;1.0,0.0;0xff42A5F5,0xffAB47BC",
"height": 350,
"showBorder": true,
"borderColor": "0xffffffff",
"borderRadius": 15,
"child": {
"type": "dy_column",
"data": {
"key": "LNHDHfHtnCtvrfr",
"children": [
{
"type": "dy_container",
"data": {
"key": "aqN0pzdSPf8Q9VV",
"child": {
"type": "dy_image",
"data": {
"key": "39IrRDiIO5loMJO",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/3.png",
"width": 250,
"height": 200,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
},
{
"type": "dy_text",
"data": {
"key": "qqHjJVx3JDhzT7R",
"text": "venusaur",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "2SR2lQU3CmESQWb",
"height": 30
}
},
{
"type": "dy_container",
"data": {
"key": "WgL2aiyCEh5YfWX",
"backgroundColor": "0xff7AC74C",
"borderRadius": 20,
"width": 250,
"padding": "10",
"showBorder": true,
"child": {
"type": "dy_text",
"data": {
"key": "RdaSht5Bm0Shryp",
"text": "Type: grass",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
}
}
}
]
}
}
}
}
}
},
{
"type": "dy_gesture_detector",
"data": {
"key": "2543654654",
"onTapAction": {
"actionString": "/moveToScreen",
"extras": {
"url": "assets/json/pokemon_detail_page?pokemon=4.json",
"urlType": "local",
"navigationType": "screen",
"navigationStyle": "push"
}
},
"child": {
"type": "dy_container",
"data": {
"key": "T8BhuiMFF7pXQV9",
"containerGradient": "-1.0,0.0;1.0,0.0;0xff42A5F5,0xffAB47BC",
"height": 350,
"showBorder": true,
"borderColor": "0xffffffff",
"borderRadius": 15,
"child": {
"type": "dy_column",
"data": {
"key": "912o1MINcdR0rxj",
"children": [
{
"type": "dy_container",
"data": {
"key": "VtAW6iDsZEXXYjB",
"child": {
"type": "dy_image",
"data": {
"key": "VZ1qUpYQ3fkGi2u",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/4.png",
"width": 250,
"height": 200,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
},
{
"type": "dy_text",
"data": {
"key": "5sT0JtGQR7om5en",
"text": "charmander",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "Slo9CC2rG8CQ0Bt",
"height": 30
}
},
{
"type": "dy_container",
"data": {
"key": "qKxPyPEdFVnndBI",
"backgroundColor": "0xffEE8130",
"borderRadius": 20,
"width": 250,
"padding": "10",
"showBorder": true,
"child": {
"type": "dy_text",
"data": {
"key": "PcPjZoUMIp2w3bQ",
"text": "Type: fire",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
}
}
}
]
}
}
}
}
}
}
],
"paginatedLoaderWidget": {
"type": "dy_loader",
"data": {
"key": "131454531",
"mainAxisAlignment": "center",
"crossAxisAlignment": "center",
"loadingText": "Grabbing More Pokemons...",
"style": {
"color": "0xFF000000",
"fontSize": 20
}
}
}
}
}
/logOut is the action handler to log out the user.
class LogoutActionHandler extends ActionHandler {
@override
void handleAction(BuildContext? context, Uri action,
Map<String, dynamic>? extras, OnHandledAction? onHandledAction) {
if (context?.mounted ?? false) {
SessionManagerState.instance.sessionStreamController.sink.add(SessionDeAuthenticatedEvent());
}
}
}
This will simply add SessionDeAuthenticatedEvent() to stream and log out.
Then we will register this action handler as well.
ServeDynamicUI.init(actionHandlers: {
RegExp(r'(^/?logOut/?$)'): LogoutActionHandler(),
});
If you see we have created one card for pokemon which we are repeating for all pokemons just data is changing which we will inflate the data in server.
{
"type": "dy_gesture_detector",
"data": {
"key": "345435345",
"onTapAction": {
"actionString": "/moveToScreen",
"extras": {
"url": "assets/json/pokemon_detail_page?pokemon=1.json",
"urlType": "local",
"navigationType": "screen",
"navigationStyle": "push",
"loaderWidgetAssetPath": "assets/json/shimmers/pokedyn_detail_page_shimmer.json"
}
},
"child": {
"type": "dy_container",
"data": {
"key": "nTxG4llzAfgFnue",
"containerGradient": "-1.0,0.0;1.0,0.0;0xff42A5F5,0xffAB47BC",
"height": 350,
"showBorder": true,
"borderColor": "0xffffffff",
"borderRadius": 15,
"child": {
"type": "dy_column",
"data": {
"key": "zd6C8y3RLOiXTRr",
"children": [
{
"type": "dy_container",
"data": {
"key": "zYuTfvGJWBcXuW5",
"child": {
"type": "dy_image",
"data": {
"key": "zeNXHx8CCuj3dlu",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png",
"width": 250,
"height": 200,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
},
{
"type": "dy_text",
"data": {
"key": "a3jiu8s4mdk1IzK",
"text": "bulbasaur",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "rJgW6Zk6YbbAADk",
"height": 30
}
},
{
"type": "dy_container",
"data": {
"key": "Ae37DJYj3s0NT1C",
"backgroundColor": "0xff7AC74C",
"borderRadius": 20,
"width": 250,
"padding": "10",
"showBorder": true,
"child": {
"type": "dy_text",
"data": {
"key": "nAWlBajbvCQoaXQ",
"text": "Type: grass",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
}
}
}
]
}
}
}
}
}
}
I am using in-built dynamic widgets no custom widgets created. So we have to add nextUrl which will get us more data when user reaches end of list.
"nextUrl": "assets/json/pokedyn_homepage?offset=5&limit=10.json"
So, We have to create Json which will get new pokemon cards.
{
"children": [
{
"type": "dy_gesture_detector",
"data": {
"key": "234423245345",
"onTapAction": {
"actionString": "/moveToScreen",
"extras": {
"url": "assets/json/pokemon_detail_page?pokemon=5.json",
"urlType": "local",
"navigationType": "screen",
"navigationStyle": "push"
}
},
"child": {
"type": "dy_container",
"data": {
"key": "xkGialzhbHsItEO",
"containerGradient": "-1.0,0.0;1.0,0.0;0xff42A5F5,0xffAB47BC",
"height": 350,
"showBorder": true,
"borderColor": "0xffffffff",
"borderRadius": 15,
"child": {
"type": "dy_column",
"data": {
"key": "6wp3vERNCMqLFCn",
"children": [
{
"type": "dy_container",
"data": {
"key": "QqelLrd3Uu44ZqD",
"child": {
"type": "dy_image",
"data": {
"key": "ePmap5kTLhqVb6G",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/5.png",
"width": 250,
"height": 200,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
},
{
"type": "dy_text",
"data": {
"key": "LSIYIdWADUcLzQB",
"text": "charmeleon",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "Z0ZQUjYGtA3ObcS",
"height": 30
}
},
{
"type": "dy_container",
"data": {
"key": "ui79b9F1yKvd7jf",
"backgroundColor": "0xffEE8130",
"borderRadius": 20,
"width": 250,
"padding": "10",
"showBorder": true,
"child": {
"type": "dy_text",
"data": {
"key": "ygT88KyymwtokxR",
"text": "Type: fire",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 20
}
}
}
}
}
]
}
}
}
}
}
}
]
}
this is structure of json which we fetch from nextUrl request call.
Now, we will put this created json and use this.
ServeDynamicUI.fromAssets(
'assets/json/pokedyn_homepage.json',
)
So, this is how you can add paginated homepage.
Now, last page is Pokemon Info Page.
On Clicking on any Pokemon Card It opens up Pokemon Info.
Lets design this page.
{
"type": "dy_scaffold",
"data": {
"key": "1236456",
"appBar": {
"pageTitle": "Pokemon Info",
"appBarGradient": "-1.0,0.0;1.0,0.0;0xff80b860,0xff619067",
"rightActions": [
{
"type": "dy_text",
"data": {
"key": "243535345",
"text": "#001",
"margin": "10,0",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 15
}
}
}
]
},
"scrollable": true,
"mainAxisAlignment": "start",
"padding": "20",
"itemsSpacing": 10,
"backgroundColor": "0xff2b292c",
"height": -1,
"children": [
{
"type": "dy_carousel",
"data": {
"key": "213234324324",
"reusable": true,
"carouselOptions": {
"autoPlay": true,
"enlargeCenterPage": true,
"scrollDirection": "horizontal",
"reverse": false,
"enableInfiniteScroll": false,
"viewportFraction": 1,
"height": 300
},
"children": [
{
"type": "dy_card",
"data": {
"key": "123454545234343",
"margin": "10",
"padding": "10",
"elevation": 10,
"linearGradient": "-1.0,0.0;1.0,0.0;0xff94cbae,0xff619067",
"borderRadius": 20,
"child": {
"type": "dy_image",
"data": {
"key": "212226323233",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png",
"width": 450,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
},
{
"type": "dy_card",
"data": {
"key": "3453453453",
"margin": "10",
"padding": "10",
"elevation": 10,
"linearGradient": "-1.0,0.0;1.0,0.0;0xff80b860,0xff619067",
"borderRadius": 20,
"child": {
"type": "dy_image",
"data": {
"key": "3534534535",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/shiny/1.png",
"width": 450,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
}
]
}
},
{
"type": "dy_container",
"data": {
"key": "345353535",
"margin": "0,20",
"child": {
"type": "dy_text",
"data": {
"key": "35353535",
"text": "Balbasaur",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 35
}
}
}
}
},
{
"type": "dy_row",
"data": {
"key": "131434",
"mainAxisAlignment": "center",
"children": [
{
"type": "dy_column",
"data": {
"key": "131434",
"children": [
{
"type": "dy_container",
"data": {
"key": "35435345345",
"backgroundColor": "0xff8eaec5",
"borderRadius": 20,
"width": 150,
"padding": "10",
"showBorder": true,
"child": {
"type": "dy_text",
"data": {
"key": "35345345",
"text": "Water",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 15
}
}
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "23424234",
"height": 25
}
},
{
"type": "dy_text",
"data": {
"key": "24243453454",
"text": "70.5 KG",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 18
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "34533535",
"height": 10
}
},
{
"type": "dy_text",
"data": {
"key": "345353534",
"text": "Weight",
"style": {
"color": "0xff555254",
"fontWeight": "bold",
"fontSize": 15
}
}
}
]
}
},
{
"type": "dy_sized_box",
"data": {
"key": "343445",
"width": 20
}
},
{
"type": "dy_column",
"data": {
"key": "131434",
"children": [
{
"type": "dy_container",
"data": {
"key": "9879345734",
"backgroundColor": "0xff719c8f",
"borderRadius": 20,
"width": 150,
"padding": "10",
"showBorder": true,
"child": {
"type": "dy_text",
"data": {
"key": "427345345",
"text": "Leaves",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 15
}
}
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "22424234234",
"height": 25
}
},
{
"type": "dy_text",
"data": {
"key": "234344345345",
"text": "1.5 M",
"style": {
"color": "0xffffffff",
"fontWeight": "bold",
"fontSize": 18
}
}
},
{
"type": "dy_sized_box",
"data": {
"key": "24245345",
"height": 10
}
},
{
"type": "dy_text",
"data": {
"key": "353535345",
"text": "Height",
"style": {
"color": "0xff555254",
"fontWeight": "bold",
"fontSize": 15
}
}
}
]
}
}
]
}
},
{
"type": "dy_container",
"data": {
"key": "234242424234",
"margin": "0,20,0,0",
"child": {
"type": "dy_text",
"data": {
"key": "245435345",
"text": "Base Stats",
"style": {
"color": "0xff8c8d8d",
"fontWeight": "bold",
"fontSize": 20
}
}
}
}
},
{
"type": "dy_column",
"data": {
"key": "134234234",
"interItemSpacing": 20,
"children": [
{
"type": "dy_percentage_indicator",
"data": {
"key": "34343434",
"type": "linear",
"linearPercentIndicator": {
"percent": 0.76,
"lineHeight": 18,
"linearGradient": "-1.0,0.0;1.0,0.0;0xFF808000,0xFFFF0000",
"animation": true,
"animationDuration": 1000,
"barRadius": 10,
"backgroundColor": "0xffffffff",
"center": {
"type": "dy_text",
"data": {
"key": "2424234",
"text": "168/300",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xff000000"
}
}
},
"leading": {
"type": "dy_container",
"data": {
"key": "2342435435435",
"width": 40,
"child": {
"type": "dy_text",
"data": {
"key": "343434",
"text": "HP",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xffffffff"
}
}
}
}
}
}
}
},
{
"type": "dy_percentage_indicator",
"data": {
"key": "24234234",
"type": "linear",
"linearPercentIndicator": {
"percent": 0.69,
"lineHeight": 18,
"animation": true,
"animationDuration": 1000,
"barRadius": 10,
"backgroundColor": "0xffffffff",
"linearGradient": "-1.0,0.0;1.0,0.0;0xFF006400,0xfffda626",
"center": {
"type": "dy_text",
"data": {
"key": "24234234234",
"text": "205/300",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xff000000"
}
}
},
"leading": {
"type": "dy_container",
"data": {
"key": "345345345345",
"width": 40,
"child": {
"type": "dy_text",
"data": {
"key": "234345345345",
"text": "ATK",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xffffffff"
}
}
}
}
}
}
}
},
{
"type": "dy_percentage_indicator",
"data": {
"key": "34343434",
"type": "linear",
"linearPercentIndicator": {
"percent": 0.92,
"lineHeight": 18,
"animation": true,
"animationDuration": 1000,
"barRadius": 10,
"backgroundColor": "0xffffffff",
"linearGradient": "-1.0,0.0;1.0,0.0;0xff0290e9,0xfffff000",
"center": {
"type": "dy_text",
"data": {
"key": "2424234",
"text": "64/300",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xff000000"
}
}
},
"leading": {
"type": "dy_container",
"data": {
"key": "02947247944753",
"width": 40,
"child": {
"type": "dy_text",
"data": {
"key": "92482428724",
"text": "DEF",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xffffffff"
}
}
}
}
}
}
}
},
{
"type": "dy_percentage_indicator",
"data": {
"key": "34343434",
"type": "linear",
"linearPercentIndicator": {
"percent": 0.66,
"lineHeight": 18,
"animation": true,
"animationDuration": 1000,
"barRadius": 10,
"backgroundColor": "0xffffffff",
"linearGradient": "-1.0,0.0;1.0,0.0;0xff8eafc4,0xff6e7ac4",
"center": {
"type": "dy_text",
"data": {
"key": "927469264876758345",
"text": "200/300",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xff000000"
}
}
},
"leading": {
"type": "dy_container",
"data": {
"key": "9836749727234",
"width": 40,
"child": {
"type": "dy_text",
"data": {
"key": "9724279472394",
"text": "SPD",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xffffffff"
}
}
}
}
}
}
}
},
{
"type": "dy_percentage_indicator",
"data": {
"key": "34343434",
"type": "linear",
"linearPercentIndicator": {
"percent": 0.85,
"lineHeight": 18,
"animation": true,
"animationDuration": 1000,
"barRadius": 10,
"backgroundColor": "0xffffffff",
"linearGradient": "-1.0,0.0;1.0,0.0;0xff378a3a,0xff8a3a37",
"center": {
"type": "dy_text",
"data": {
"key": "2424234",
"text": "295/1000",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xff000000"
}
}
},
"leading": {
"type": "dy_container",
"data": {
"key": "23345345345",
"width": 40,
"child": {
"type": "dy_text",
"data": {
"key": "3534534535",
"text": "EXP",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xffffffff"
}
}
}
}
}
}
}
}
]
}
}
]
}
}
Main widgets here are the carousel and percent indicator. serve_dynamic_ui uses carousel_slider and percent_indicator and create dy_carousel and dy_percent_indicator as wrapper over these widget for dynamic widgets.
{
"type": "dy_carousel",
"data": {
"key": "213234324324",
"reusable": true,
"carouselOptions": {
"autoPlay": true,
"enlargeCenterPage": true,
"scrollDirection": "horizontal",
"reverse": false,
"enableInfiniteScroll": false,
"viewportFraction": 1,
"height": 300
},
"children": [
{
"type": "dy_card",
"data": {
"key": "123454545234343",
"margin": "10",
"padding": "10",
"elevation": 10,
"linearGradient": "-1.0,0.0;1.0,0.0;0xff94cbae,0xff619067",
"borderRadius": 20,
"child": {
"type": "dy_image",
"data": {
"key": "212226323233",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png",
"width": 450,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
},
{
"type": "dy_card",
"data": {
"key": "3453453453",
"margin": "10",
"padding": "10",
"elevation": 10,
"linearGradient": "-1.0,0.0;1.0,0.0;0xff80b860,0xff619067",
"borderRadius": 20,
"child": {
"type": "dy_image",
"data": {
"key": "3534534535",
"src": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/shiny/1.png",
"width": 450,
"imageType": "network",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
}
]
}
}
In the dy_carousel I have added dy_card.
"carouselOptions": {
"autoPlay": true,
"enlargeCenterPage": true,
"scrollDirection": "horizontal",
"reverse": false,
"enableInfiniteScroll": false,
"viewportFraction": 1,
"height": 300
}
these are the options i have set you can set your own.
Now, we create dy_percent_indicator
{
"type": "dy_percentage_indicator",
"data": {
"key": "24234234",
"type": "linear",
"linearPercentIndicator": {
"percent": 0.69,
"lineHeight": 18,
"animation": true,
"animationDuration": 1000,
"barRadius": 10,
"backgroundColor": "0xffffffff",
"linearGradient": "-1.0,0.0;1.0,0.0;0xFF006400,0xfffda626",
"center": {
"type": "dy_text",
"data": {
"key": "24234234234",
"text": "205/300",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xff000000"
}
}
},
"leading": {
"type": "dy_container",
"data": {
"key": "345345345345",
"width": 40,
"child": {
"type": "dy_text",
"data": {
"key": "234345345345",
"text": "ATK",
"style": {
"fontSize": 15,
"fontWeight": "bold",
"color": "0xffffffff"
}
}
}
}
}
}
}
}
This shows one of the stat. we add 4 stats about a pokemon.
This is how we build json pages.
Now, last is to add shimmer pages.
{
"type": "dy_shimmer_stack",
"data": {
"key": "2234656231",
"alignment": "center",
"shimmerBaseColor": "0xff3b3b3b",
"height": 300,
"borderRadius": 20,
"margin": "10",
"children": [
{
"type": "dy_positioned",
"data": {
"key": "131313",
"child": {
"type": "dy_container",
"data": {
"key": "234234234",
"containerGradient": "-1.0,0.0;1.0,0.0;0xff42A5F5,0xffAB47BC",
"borderColor": "0xff000000",
"borderRadius": 20,
"margin": "10",
"showBorder": true,
"child": {
"type": "dy_image",
"data": {
"key": "242424234234",
"src": "assets/images/ic_pokemon_placeholder.png",
"width": 350,
"height": 300,
"imageType": "asset",
"fit": "fill",
"placeholderImagePath": "assets/images/ic_pokemon_placeholder.png"
}
}
}
}
}
}
]
}
}
This is the card shimmer stack which we will show while we fetch our page json from network.
these shimmer pages path we can add using templateStringJson.
You can find full shimmer page jsons here.
To know more about Shimmers read Part-4
This article has become very long I will write its continuation in next article where we only talk about developing backend using serverpod for our app.
Thank You For Reading Article
Server-Driven UI in Flutter from Scratch: Part 5 — Building Complete App End-to-End using Serverpod was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Sk Ahron
Sk Ahron | Sciencx (2024-07-28T17:02:40+00:00) Server-Driven UI in Flutter from Scratch: Part 5 — Building Complete App End-to-End using Serverpod. Retrieved from https://www.scien.cx/2024/07/28/server-driven-ui-in-flutter-from-scratch-part-5-building-complete-app-end-to-end-using-serverpod/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.