How To Search Django Api And Display Items Using Flutter

We show you how you can easily query a Django rest Api and display content on your flutter application following the Bloc pattern.

Scenario: You have a flutter application that consumes data from an Api, you may wish to make such api searchable. Your app users are able to enter a search term and return results from your API based on that search term.

Step 1: Make sure you have an api running, in our case we have our Django Rest Api live and running.

A snippet of how to make your Django Api Queryable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class BlogsListAPIView(ListAPIView):
    model = Blog
    queryset = Blog.objects.all()

    serializer_class = BlogSerializer
    filter_backends= [SearchFilter, OrderingFilter]
    permission_classes = [AllowAny]
    search_fields = ['blog_title', 'blog_description', 'user__first_name']
    pagination_class = BlogPageNumberPagination 

    def get_queryset(self, *args, **kwargs):
        queryset_list = Blog.objects.all()
        query = self.request.GET.get("q")
        if query:
            queryset_list = queryset_list.filter(
                    Q(title__icontains=query)|
                    Q(content__icontains=query)|
                    Q(user__first_name__icontains=query) |
                    Q(user__last_name__icontains=query)
                    ).distinct()
        return queryset_list

As you can see from our Django rest api view class,  'blog_title', 'blog_description', 'userfirst_name'] are queryable fields. We are making an assumption that you are able to program your Django Rest Api. But if you should have any questions, leave a comment below.

Step 2: Querying your api from Flutter and displaying the results.

This is based on the flutter example of search. We will be using the Bloc pattern, Bloc pattern is a Google recommended way of building your flutter applications. Its a state management system for Flutter recommended by Google developers. It helps in managing state and make access to data from a central place in your projects.You can read more about this online. A lot of tutorials are available online to guide you through.

a. Our blog Model, you make it flutter friendly for getting our api results:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//blog.model.dart
class BlogResultModel {
  final String error;
  int count;
  String next;
  dynamic previous;
  List<Blog> results;

  BlogResultModel({
    this.count,
    this.next,
    this.previous,
    this.results,
    this.error
  });

  factory BlogResultModel.fromJson(Map<String, dynamic> json) {
    return BlogResultModel(
      count: json['count'],
      next: json['next'],
      previous: json['previous'],
      results: _parseResult(json['results']),
      error: ""
    );
  }

  BlogResultModel.withError(String errorValue)
      : results = List<Blog>(), error = errorValue;

bool get isPopulated => results.isNotEmpty;

bool get isEmpty => results.isEmpty;
}

_parseResult(List<dynamic> data) {
  List<Blog> results = new List<Blog>();
  data.forEach((item) {
    results.add(Blog.fromJson(item));
  });
  return results;
}
class Blog  extends BlogResultModel{
  String  blogid;
  String  blogtitle;
  String  blogdescription;
  Blog(
      {this.blogid,
      this.blogtitle,
      this.blogdescription,
      });

  factory Blog.fromJson(Map<String, dynamic> json) {
    return Blog(
      blogid: json['blog_id'],
      blogtitle: json['blog_title'],
      blogdescription: json['blog_description'],
    );
  }
}

b. Next, we set up our blog api, to query data from Django rest api.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//blog.api.dart
class BlogApi {
  final String baseUrl;
  final Map<String, BlogResultModel> cache;
  final http.Client client;

  BlogApi({
    HttpClient client,
    Map<String, BlogResultModel> cache,
    this.baseUrl = ["ENTER YOUR API URL"],
  })  : this.client = client ?? http.Client(),
        this.cache = cache ?? <String, BlogResultModel>{};

  Future<BlogResultModel> search(String term) async {
    if (cache.containsKey(term)) {
      return cache[term];
    } else {
      final result = await _fetchResults(term);

      cache[term] = result;

      return result;
    }
  }
}

c. Let's add our bloc pattern, to connect our frontend to our backend api.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//blog.bloc.dart
class BlogBloc extends BlogApi implements Global{
  final Sink<String> onTextChanged;
  final Stream<BlogState> state;

  factory BlogBloc(BlogApi api) {
    final onTextChanged = PublishSubject<String>();

    final state = onTextChanged
        // If the text has not changed, do not perform a new search
        .distinct()
        // Wait for the user to stop typing for 250ms before running a search
        .debounceTime(const Duration(milliseconds: 250))
        // Call the api with the given search term and convert it to a
        // State. If another search term is entered, flatMapLatest will ensure
        // the previous search is discarded so we don't deliver stale results
        // to the View.
        .switchMap<BlogState>((String term) => _search(term, api))
        // The initial state to deliver to the screen.
        .startWith(BlogNoTerm());

    return BlogBloc._(onTextChanged, state);

   void dispose() async{
    sink.close();
    onTextChanged.close();
   }
  }

As you can read from the comments: If the text has not changed, do not perform a new search, we want to avoid continuous query of our backend. We also do this, by waiting until the user has stopped typing (250ms) then we do our next query. d. Let's add a blog state file. This manages our app state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//blog-state.dart
class BlogState {}
class BlogLoading extends BlogState {}
class BlogError extends BlogState {}

class BlogNoTerm extends BlogState {}

class BlogPopulated extends BlogState {
  final BlogResultModel result;

  BlogPopulated(this.result);
}
class BlogEmpty extends BlogState {}
</pre><h2>f. Time to move to the front-end, we need widgets for each state.</h2><p>Entry start or Intro state, Error start if our API returns an error, Empty State widget for empty query results and also a widget for results should our query match some results from our api.</p><p>i. Entry or Intro state widget:</p><pre class="ql-syntax" spellcheck="false">//dart
//entry_state_widget.dart
class BlogIntro extends StatelessWidget {
  const BlogIntro({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: FractionalOffset.center,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Icon(Icons.info, color: Colors.green[200], size: 80.0),
          Container(
            padding: EdgeInsets.only(top: 16.0),
            child: Text(
              "Enter a search term to begin",
              style: TextStyle(
                color: Colors.green[100],
              ),
            ),
          )
        ],
      ),
    );
  }
}

ii . Results widget incase we find matching query results:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//dart
//results_widget.dart
class BlogResultWidget extends StatelessWidget {
  final List<Blog> items;

  const BlogResultWidget({Key key, @required this.items}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        final item = items[index];

        return Card(
           child: Text(item.blogtitle)
       )
     }
)
}

iii. Widget for API error

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//dart
//error_api_widget.dart
class BlogErrorWidget extends StatelessWidget {
  const BlogErrorWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: FractionalOffset.center,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Icon(Icons.error_outline, color: Colors.red[300], size: 80.0),
          Container(
            padding: EdgeInsets.only(top: 16.0),
            child: Text(
              "Something is not right! API",
              style: TextStyle(
                color: Colors.red[300],
              ),
            ),
          )
        ],
      ),
    );
  }
}

iv. The loading widget, which is just Circular progress indicator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//dart
//loading_widget.dart
class LoadingWidget extends StatelessWidget {
  const LoadingWidget({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: FractionalOffset.center,
      child: CircularProgressIndicator(),
    );
  }
}

g. Display results to a flutter application screen:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
//dart
//app-blogs.dart
class BlogsScreen extends StatefulWidget {
  final BlogApi api;

  BlogsScreen({Key key, this.api}) : super(key: key);

  @override
  BlogsScreenState createState() {
    return BlogsScreenState();
  }
}

class BlogsScreenState extends State<BlogsScreen> {
  SearchBloc bloc;
  FocusNode _focus = new FocusNode(); 
  @override
  void initState() {
    super.initState();

    bloc = BlogBloc(widget.api);
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<BlogState>(
      stream: bloc.state,
      initialData: BlogNoTerm(),
      builder: (BuildContext context, AsyncSnapshot<BlogState> snapshot) {
        final state = snapshot.data;

        return Scaffold(
          appBar: appBar(context, KjobbersAppTheme.green), //AppTopNav(au,
          body: Stack(
            children: <Widget>[
              Flex(direction: Axis.vertical, children: <Widget>[
                Container(
            height: 50,
            child: Card(
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                mainAxisAlignment: MainAxisAlignment.start,
                mainAxisSize: MainAxisSize.max,
                children: <Widget>[
                  Expanded(
                      child: TextField(
                    focusNode: _focus,
                    style: TextStyle(fontSize: 16.0),
                    decoration: InputDecoration(
                          contentPadding:
                              EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
                          prefixIcon: Icon(Icons.search),
                          hintText: "Enter Search..",
                          border: InputBorder.none,
                          focusedBorder: OutlineInputBorder(
                              borderSide:
                                  BorderSide(color: KjobbersAppTheme.grey300, width: 32.0),
                              borderRadius: BorderRadius.circular(5.0))),
                              autofocus: true,
                              onChanged: bloc.onTextChanged.add,
                  )),
                  IconButton(
                    icon: Icon(Icons.filter_list),
                    onPressed: () {},
                  ),
                ],
              ),
            ),
          ),
                Expanded(
                  child: AnimatedSwitcher(
                    duration: const Duration(milliseconds: 300),
                    child: _buildChild(state),
                  ),
                )
              ])
            ],
          ),
         bottomNavigationBar: AppFooterNav(context, 0, KjobbersAppTheme.green),
        );
      },
    );
  }

  Widget _buildChild(BlogState state) {
    if (state is BlogNoTerm) {
      return BlogIntro();
    } else if (state is BlogEmpty) {
      return EmptyWidget();
    } else if (state is BlogLoading) {
      return LoadingWidget();
    } else if (state is BlogError) {
      return BlogErrorWidget();
    } else if (state is BlogPopulated) {
      return BlogResultWidget(items: state.result.items);
    }

    throw Exception('${state.runtimeType} is not supported');
  }
}


Reference:

https://github.com/ReactiveX/rxdart


Related Posts

0 Comments

12345

    00