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.
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.
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],
),
),
)
],
),
);
}
}
|
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)
)
}
)
}
|
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],
),
),
)
],
),
);
}
}
|
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(),
);
}
}
|
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