Search code examples
flutterflutter-listviewflutter-list-tile

Can I add other widget below list tile


I am trying to achieve this:

This

But as I m new to this don't understand how to implement. I check many code but nothing help me so please help me. I am beginner in the coding.

In this code I use json file and I am able to display list tile but as shown in the image I want to add some text and icon below this list tile but when I add any widget it goes side to leading

I don't understand how do I do this

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List _items = [];

  // Fetch content from the json file
  Future<void> readJson() async {
    final String response = await rootBundle.loadString('assets/sample.json');
    final data = await json.decode(response);
    setState(() {
      _items = data["items"];
    });
  }

  @override
  void initState() {
    super.initState();
    // Call the readJson method when the app starts
    readJson();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: const Icon(Icons.arrow_back, color: Colors.black),
          onPressed: () => Navigator.of(context).pop(),
        ),
        backgroundColor: Colors.white,
        centerTitle: true,
        elevation: 0,
        title: const Text(
          'Ask Help',
          style: TextStyle(color: Colors.black),
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
        child: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Column(
                  children: <Widget>[
                    ElevatedButton(
                      onPressed: () {},
                      style: ElevatedButton.styleFrom(
                        //change width and height on your need width = 200 and height = 50
                        minimumSize: Size(160, 50),
                      ),
                      child: const Text('Patient'),
                    )
                  ],
                ),
                Column(
                  children: <Widget>[
                    OutlinedButton(
                      style: OutlinedButton.styleFrom(
                          side: const BorderSide(color: Colors.blue, width: 1),
                          minimumSize: Size(160, 50)),
                      onPressed: () {},
                      child: const Text(
                        "NGO",
                        style: TextStyle(color: Colors.blue),
                      ),
                    ),
                  ],
                )
              ],
            ),
            // Display the data loaded from sample.json
            Container(
              child: _items.isNotEmpty
                  ? Expanded(
                      child: ListView.separated(
                        itemCount: _items.length,
                        separatorBuilder: (BuildContext context, int index) =>
                            Divider(height: 1),
                        itemBuilder: (context, index) {
                          return Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: ListTile(
                              leading: CircleAvatar(
                                child:
                                    Text(_items[index]["imageUrl"].toString()),
                              ),
                              title: Text(_items[index]["name"]),
                              subtitle: Column(
                                  mainAxisAlignment: MainAxisAlignment.start,
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: <Widget>[
                                    Text(_items[index]["phone_number"],
                                        style: const TextStyle(
                                            fontSize: 13.0,
                                            fontWeight: FontWeight.normal)),
                                    Text(_items[index]["email_id"],
                                        style: const TextStyle(
                                            fontSize: 13.0,
                                            fontWeight: FontWeight.normal)),
                                    // Text(
                                    //   'Population: ${_items[index]["email_id"]}',
                                    //   style: const TextStyle(
                                    //       fontSize: 11.0,
                                    //       fontWeight: FontWeight.normal),
                                    // ),
                                    
                                  ]
                                  ),
                              trailing: const Icon(Icons.more_vert),
                              
                            ),
                            
                          );
                        },
                      ),
                    )
                  : Container(
                    //color: Colors.amber,
                  ),
            ),

            Align(
              alignment: Alignment.bottomCenter,
              child: Container(
                height: 70,
                child: Center(
                  child: ElevatedButton(
                    onPressed: () {},
                    style: ElevatedButton.styleFrom(
                      //change width and height on your need width = 200 and height = 50
                      minimumSize: Size(300, 50),
                    ),
                    child: const Text('Register'),
                  ),
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}
     

My json code:

    {
    "items": [
        {
            "id": "p1",
            "name": "Item 1",
            "phone_number":"8975412369",
            "email_id":"[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p2",
            "name": "Item 2",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p3",
            "name": "Item 3",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p1",
            "name": "Item 1",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p2",
            "name": "Item 2",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p3",
            "name": "Item 3",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p1",
            "name": "Item 1",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p2",
            "name": "Item 2",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p3",
            "name": "Item 3",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },{
            "id": "p1",
            "name": "Item 1",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p2",
            "name": "Item 2",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        },
        {
            "id": "p3",
            "name": "Item 3",
            "phone_number": "8975412369",
            "email_id": "[email protected]",
            "description": "Description 1",
            "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
        }
    ]
}

Solution

  • The answer to the question is yes! Here's a minimal example:

    /* SAMPLE
            {
                "id": "p1",
                "name": "Item 1",
                "phone_number":"8975412369",
                "email_id":"[email protected]",
                "description": "Description 1",
                "imageUrl": "https://m.media-amazon.com/images/I/81S-ekaE+vS._AC_UL320_.jpg"
            },
    */
    class Item {
      Item({
        required this.id,
        required this.name,
        required this.phoneNumber,
        required this.emailId,
        required this.description,
        required this.imageUrl,
      });
    
      final String id;
      final String name;
      final String phoneNumber;
      final String emailId;
      final String description;
      final String imageUrl;
    
      factory Item.fromJson(Map<String, dynamic> json) => Item(
            id: json["id"],
            name: json["name"],
            phoneNumber: json["phone_number"],
            emailId: json["email_id"],
            description: json["description"],
            imageUrl: json["imageUrl"],
          );
    
      Map<String, dynamic> toJson() => {
            "id": id,
            "name": name,
            "phone_number": phoneNumber,
            "email_id": emailId,
            "description": description,
            "imageUrl": imageUrl,
          };
    
      static List<Item> fromJsonList(List<Map<String,dynamic>> jsonList) {
        final List<Item> items = [];
        for (var json in jsonList) {
          items.add(Item.fromJson(json));
        }
        return items;
      }
    }
    

    The above class goes in data_model.dart file. I have made a data model for you. Before working on UI, it's best to organize your code into different parts.

    There is also this custom widget made for easier handling of stuff and the DRY rule! This goes to the details.dart file.

    import 'package:flutter/material.dart';
    
    import 'data_model.dart';
    
    class DetailsPage extends StatelessWidget {
      const DetailsPage({super.key, required this.item});
      final Item item;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            leading: IconButton(
              icon: const Icon(Icons.arrow_back, color: Colors.black),
              onPressed: () => Navigator.of(context)
                  .pop(), // No need to pop for now, there is nothing behind this screen :)
            ),
            backgroundColor: Colors.white,
            centerTitle: true,
            elevation: 0,
            title: const Text(
              'Ask Help',
              style: TextStyle(color: Colors.black),
            ),
          ),
          body: LayoutBuilder(builder: (context, constraints) {
            return SingleChildScrollView(
              child: ConstrainedBox(
                constraints: constraints,
                child: Column(
                  children: [
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 12),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: <Widget>[
                          ElevatedButton(
                            onPressed: () {},
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.teal.withBlue(200),
                              //change width and height on your need width = 200 and height = 50
                              minimumSize: const Size(160, 50),
                            ),
                            child: const Text('Patient'),
                          ),
                          OutlinedButton(
                            style: OutlinedButton.styleFrom(
                                side:
                                    const BorderSide(color: Colors.teal, width: 1),
                                minimumSize: const Size(160, 50)),
                            onPressed: () {},
                            child: const Text(
                              "NGO",
                              style: TextStyle(color: Colors.blue),
                            ),
                          ),
                        ],
                      ),
                    ),
                    const SizedBox(
                      height: 20,
                    ),
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 12),
                      child: DetailSection(item: item, viewAttachment: true),
                    ),
                    const SizedBox(
                      height: 10,
                    ),
                    const Divider(
                      color: Colors.black26,
                    ),
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 12),
                      child: DetailSection(
                          item: item,
                          viewAttachment: false,
                          skills: const ['plumber', '2 hrs']),
                    ),
                    const Divider(
                      color: Colors.black26,
                    ),
                    const Spacer(),
                    Align(
                      alignment: Alignment.bottomCenter,
                      child: SizedBox(
                        height: 70,
                        child: Center(
                          child: ElevatedButton(
                            onPressed: () {},
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.teal.withBlue(200),
                              minimumSize: const Size(300, 50),
                            ),
                            child: const Text('Register'),
                          ),
                        ),
                      ),
                    )
                  ],
                ),
              ),
            );
          }),
        );
      }
    }
    
    class DetailSection extends StatelessWidget {
      final Item item;
      final List<String> skills;
      final bool viewAttachment;
      const DetailSection(
          {Key? key,
          required this.item,
          required this.viewAttachment,
          this.skills = const []})
          : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            ListTile(
              leading: CircleAvatar(
                radius: 35,
                backgroundColor: Colors.transparent,
                child: Image.network(item.imageUrl),
              ),
              title: Text(
                item.name,
                style: const TextStyle(fontWeight: FontWeight.bold),
              ),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(item.phoneNumber,
                      style: const TextStyle(
                          fontSize: 13.0, fontWeight: FontWeight.normal)),
                  Text(item.emailId,
                      style: const TextStyle(
                          fontSize: 13.0, fontWeight: FontWeight.normal)),
                ],
              ),
            ),
            const SizedBox(
              height: 10,
            ),
            if (skills.isNotEmpty) ...[
              const Align(
                alignment: Alignment.centerLeft,
                child: Text(
                  'Skills',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
              ),
              const SizedBox(
                height: 15,
              ),
              Row(
                children: skills
                    .map((e) => Container(
                        margin: const EdgeInsets.only(
                          right: 10,
                          bottom: 10,
                        ),
                        padding: const EdgeInsets.all(2),
                        decoration: BoxDecoration(
                            color: Colors.amberAccent.withOpacity(0.5),
                            borderRadius:
                                const BorderRadius.all(Radius.circular(12))),
                        child: Padding(
                          padding: const EdgeInsets.symmetric(horizontal: 6.0),
                          child: Text(e),
                        )))
                    .toList(),
              )
            ],
            const Align(
              alignment: Alignment.centerLeft,
              child: Text(
                'Needs Help for',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
            ),
            const SizedBox(
              height: 10,
            ),
            const Align(
                alignment: Alignment.centerLeft,
                child: Text('Bluh bluh aaaaaaah I need help!')),
            const SizedBox(
              height: 10,
            ),
            if (viewAttachment)
              Row(
                children: const [
                  Text(
                    'View Attachment',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  Spacer(),
                  Icon(Icons.play_arrow_outlined),
                ],
              ),
          ],
        );
      }
    }
    

    Edited main.dart:

    import 'dart:convert';
    
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'details.dart';
    
    import 'data_model.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          debugShowCheckedModeBanner: false,
          home: HomePage(),
        );
      }
    }
    
    class HomePage extends StatefulWidget {
      const HomePage({super.key});
    
      @override
      State<HomePage> createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      final List<Item> _items =
          []; // Don't use dynamic types. List<T> where T is your type is the better form :)
    
      // Fetch content from the json file
      Future<void> readJson() async {
        final String response = await rootBundle.loadString('assets/sample.json');
        // jsonDecode/json.decode are not futures. No need to "await" them.
        final rawItems = json.decode(response)['items'];
        final jsonList = rawItems.cast<Map<String, dynamic>>().toList();
        final items = Item.fromJsonList(jsonList);
        setState(() {
          _items.addAll(items);
        });
      }
    
      @override
      void initState() {
        super.initState();
        // Call the readJson method when the app starts
        readJson();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.white,
            centerTitle: true,
            elevation: 0,
            title: const Text(
              'Item List',
              style: TextStyle(color: Colors.black),
            ),
          ),
          body: Padding(
            padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
            child: Container(
              child: _items.isNotEmpty
                  ? ListView.separated(
                      itemCount: _items.length,
                      separatorBuilder: (BuildContext context, int index) =>
                          const Divider(height: 1),
                      itemBuilder: (context, index) {
                        final item = _items[
                            index]; // No need to use [index] for each item :)
                        return Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: ListTile(
                            leading: CircleAvatar(
                              child: Image.network(item
                                  .imageUrl), // Optionally, use cached_network_image package.
                            ),
                            title: Text(item.name),
                            subtitle: Column(
                                mainAxisAlignment: MainAxisAlignment.start,
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  Text(item.phoneNumber,
                                      style: const TextStyle(
                                          fontSize: 13.0,
                                          fontWeight: FontWeight.normal)),
                                  Text(item.emailId,
                                      style: const TextStyle(
                                          fontSize: 13.0,
                                          fontWeight: FontWeight.normal)),
                                ]),
                            trailing: const Icon(Icons.more_vert),
                            onTap: () =>
                                Navigator.of(context).push(MaterialPageRoute(
                                    builder: ((context) => DetailsPage(
                                          item: item,
                                        )))),
                          ),
                        );
                      },
                    )
                  : const CircularProgressIndicator(),
            ),
          ),
        );
      }
    }
    
    

    I have modified the UI code to represent the model changes.

    Your code and JSON sample are a bit far from the linked image you want to implement, but I have used made-up details to represent a better example to you. Feel free to play with the code and UI.